templates/CDKTemplate.js (221 lines of code) (raw):

/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0 or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const { Stack, Duration, App } = require('aws-cdk-lib'); const lambda = require( 'aws-cdk-lib/aws-lambda'); const iam = require( 'aws-cdk-lib/aws-iam'); const ec2 = require( 'aws-cdk-lib/aws-ec2'); const { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema, CfnDataSource, CfnResolver, CfnFunctionConfiguration } = require( 'aws-cdk-lib/aws-appsync'); const NAME = ''; const REGION = ''; const NEPTUNE_HOST = ''; const NEPTUNE_PORT = ''; const NEPTUNE_DB_NAME = ''; const NEPTUNE_TYPE = ''; const NEPTUNE_DBSubnetGroup = null; const NEPTUNE_DBSubnetIds = null; const NEPTUNE_VpcSecurityGroupId = null; const NEPTUNE_IAM_AUTH = false; const NEPTUNE_IAM_POLICY_RESOURCE = '*'; const LAMBDA_FUNCTION_NAME = ''; const LAMBDA_ZIP_FILE = ''; let LAMBDA_ARN = ''; const APPSYNC_SCHEMA = ''; const APPSYNC_ATTACH_QUERY = []; const APPSYNC_ATTACH_MUTATION = []; const MIN_HOST_PARTS = 5; const NUM_DOMAIN_PARTS = 3; const HOST_DELIMITER = '.'; class AppSyncNeptuneStack extends Stack { /** * * @param {Construct} scope * @param {string} id * @param {StackProps=} props */ constructor(scope, id, props) { super(scope, id, props); // Lambda function IAM/VPC let echoLambda = null; // Lambda: IAM Role const lambda_role = new iam.Role(this, NAME + 'LambdaExecutionRole', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com') }); lambda_role.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')); let env = { NEPTUNE_HOST: NEPTUNE_HOST, NEPTUNE_PORT: NEPTUNE_PORT, NEPTUNE_IAM_AUTH_ENABLED: NEPTUNE_IAM_AUTH.toString(), LOGGING_ENABLED: 'false', NEPTUNE_DB_NAME: NEPTUNE_DB_NAME, NEPTUNE_REGION: REGION, NEPTUNE_DOMAIN: this.parseNeptuneDomainFromHost(NEPTUNE_HOST), NEPTUNE_TYPE: NEPTUNE_TYPE, }; if (NEPTUNE_IAM_AUTH) { // is IAM auth echoLambda = new lambda.Function(this, LAMBDA_FUNCTION_NAME, { functionName: LAMBDA_FUNCTION_NAME, description: 'Neptune GraphQL Resolver for AppSync', code: lambda.Code.fromAsset(LAMBDA_ZIP_FILE), handler: 'index.handler', runtime: lambda.Runtime.NODEJS_18_X, timeout: Duration.seconds(15), memorySize: 128, environment: env, initialPolicy: [new iam.PolicyStatement({ sid: NAME + "NeptuneQueryPolicy", effect: iam.Effect.ALLOW, actions: [ NEPTUNE_TYPE + ':connect', NEPTUNE_TYPE + ':DeleteDataViaQuery', NEPTUNE_TYPE + ':ReadDataViaQuery', NEPTUNE_TYPE + ':WriteDataViaQuery' ], resources: [NEPTUNE_IAM_POLICY_RESOURCE] })], roleArn: lambda_role.roleArn }); } else { // is VPC auth const neptune_vpc = ec2.Vpc.fromLookup(this, 'Neptune_VPC', { vpcId: NEPTUNE_DBSubnetGroup }); lambda_role.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole')); let subnets = NEPTUNE_DBSubnetIds.split(',').map((subnetId) => ec2.Subnet.fromSubnetId(this, 'neptuneSubnet-' + subnetId, subnetId)); echoLambda = new lambda.Function(this, LAMBDA_FUNCTION_NAME, { functionName: LAMBDA_FUNCTION_NAME, description: 'Neptune GraphQL Resolver for AppSync', code: lambda.Code.fromAsset(LAMBDA_ZIP_FILE), handler: 'index.handler', runtime: lambda.Runtime.NODEJS_18_X, timeout: Duration.seconds(15), memorySize: 128, environment: env, vpc: neptune_vpc, vpcSubnets: { subnets: subnets }, securityGroups: [ ec2.SecurityGroup.fromSecurityGroupId(this, 'neptuneSecurityGroup', NEPTUNE_VpcSecurityGroupId) ], allowPublicSubnet: 'true', roleArn: lambda_role.roleArn }); } echoLambda.node.addDependency(lambda_role); LAMBDA_ARN = echoLambda.functionArn; // Appsync: GraphQL API const itemsGraphQLApi = new CfnGraphQLApi(this, NAME + 'API', { name: NAME + 'API', authenticationType: 'API_KEY' }); // AppSync: Key new CfnApiKey(this, NAME + '-' + 'APIKEY', { apiId: itemsGraphQLApi.attrApiId }); // AppSync: Schema const apiSchema = new CfnGraphQLSchema(this, NAME + 'Schema', { apiId: itemsGraphQLApi.attrApiId, definition: APPSYNC_SCHEMA }); // AppSync: IAM Lambda invocation role const lambdaInvokationRole = new iam.Role(this, NAME + 'LambdaInvocationRole', { assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com') }); lambdaInvokationRole.addToPolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["lambda:invokeFunction"], resources: [ LAMBDA_ARN, LAMBDA_ARN + ":*" ] }) ); // AppSync: DataSource const dataSource = new CfnDataSource(this, NAME + 'DataSource', { apiId: itemsGraphQLApi.attrApiId, name: NAME + 'DataSource', type: 'AWS_LAMBDA', lambdaConfig: { lambdaFunctionArn: LAMBDA_ARN, }, serviceRoleArn: lambdaInvokationRole.roleArn }); dataSource.node.addDependency(itemsGraphQLApi); dataSource.node.addDependency(lambdaInvokationRole); dataSource.node.addDependency(echoLambda); // AppSync: Function const functionappSync = new CfnFunctionConfiguration(this, NAME + 'Function', { apiId: itemsGraphQLApi.attrApiId, dataSourceName: NAME + 'DataSource', name: NAME + 'Function', runtime: { name: "APPSYNC_JS", runtimeVersion: "1.0.0", }, code: `import { util } from '@aws-appsync/utils'; export function request(ctx) { const {source, args} = ctx return { operation: 'Invoke', payload: { field: ctx.info.fieldName, arguments: args, selectionSetGraphQL: ctx.info.selectionSetGraphQL, source }, }; } export function response(ctx) { return ctx.result; }` }); functionappSync.node.addDependency(dataSource); // AppSync: attach resolvers to queries APPSYNC_ATTACH_QUERY.forEach(n => { const resolver = new CfnResolver(this, n + "Resolver", { apiId: itemsGraphQLApi.attrApiId, typeName: 'Query', fieldName: n, kind: "PIPELINE", pipelineConfig: { functions: [ functionappSync.attrFunctionId ]}, runtime: { name: "APPSYNC_JS", runtimeVersion: "1.0.0", }, code: `import {util} from '@aws-appsync/utils'; export function request(ctx) { return {}; } export function response(ctx) { return ctx.prev.result; }` }); resolver.node.addDependency(functionappSync); resolver.node.addDependency(apiSchema); }); // AppSync: attach resolvers to mutations APPSYNC_ATTACH_MUTATION.forEach(n => { const resolver = new CfnResolver(this, n + "Resolver", { apiId: itemsGraphQLApi.attrApiId, typeName: 'Mutation', fieldName: n, kind: "PIPELINE", pipelineConfig: { functions: [ functionappSync.attrFunctionId ]}, runtime: { name: "APPSYNC_JS", runtimeVersion: "1.0.0", }, code: `import {util} from '@aws-appsync/utils'; export function request(ctx) { return {}; } export function response(ctx) { return ctx.prev.result; }` }); resolver.node.addDependency(functionappSync); resolver.node.addDependency(apiSchema); }); } parseNeptuneDomainFromHost(neptuneHost) { let parts = neptuneHost.split(HOST_DELIMITER); if (parts.length < MIN_HOST_PARTS) { throw Error('Cannot parse neptune host ' + neptuneHost + ' because it has ' + parts.length + ' part(s) delimited by ' + HOST_DELIMITER + ' but expected at least ' + MIN_HOST_PARTS); } // last 3 parts of the host make up the domain // ie. neptune.amazonaws.com or neptune-graph.amazonaws.com let domainParts = parts.splice(parts.length - NUM_DOMAIN_PARTS, NUM_DOMAIN_PARTS); return domainParts.join(HOST_DELIMITER); } } module.exports = { AppSyncNeptuneStack }