Why is PrivateWorkForce on AWS failing to delete?

0

Hi Everyone,

I'm trying to create an AWS Custom Labels Cloudstack via the template provided by AWS:

https://ml-specialist-sa-demo-us-east-2.s3.us-east-2.amazonaws.com/custom-brand-detection/1.0.0/amazon-rekognition-custom-brand-detection.template

At first, I ran into the following issue which was displayed in the console:

Resource handler returned message: "The runtime parameter of nodejs10.x is no longer supported for creating or updating AWS Lambda functions. We recommend you use the new runtime (nodejs18.x) while creating or updating functions.

I updated the Node version to ^20 as well as the aws-sdk's. I rebuilt, deployed and attempted to create the stack but it failed because a previous resource wouldn't delete and the console stated the following reason:

Received response status [FAILED] from custom resource. Message returned: Cannot find module 'aws-sdk' Require stack: - /var/task/lib/sagemaker/privateWorkforce.js - /var/task/lib/sagemaker/index.js - /var/task/index.js - /var/runtime/index.mjs.

I've also tried deleting the previous stacks but I'm still getting this error.

I've been looking around but can't seems to find an answer for this and attempted to reach out to AWS for an updated template but they provided a template that was also out of date so I'm not sure what to do at this point. If anyone has suggestions or point me in the right direction, I'd appreciate it.

Thanks,

Nick

Index.js

    const PrivateWorkforce = require('./privateWorkforce');

exports.PrivateWorkforceConfiguration = async (event, context) => {
  try {
    const workteam = new PrivateWorkforce(event, context);
    return workteam.isRequestType('Delete')
      ? workteam.deleteResource()
      : workteam.isRequestType('Update')
        ? workteam.updateResource()
        : workteam.createResource();
  } catch (e) {
    e.message = `PrivateWorkforceConfiguration: ${e.message}`;
    throw e;
  }
};

privateworkforce.js

const FS = require('fs');
const PATH = require('path');
const AWS = require('aws-sdk');

const mxBaseResponse = require('../shared/mxBaseResponse');

class PrivateWorkforce extends mxBaseResponse(class {}) {
  constructor(event, context) {
    super(event, context);
    /* sanity check */
    const data = event.ResourceProperties.Data;
    this.sanityCheck(data);
    this.$data = data;

    this.$cognito = new AWS.CognitoIdentityServiceProvider({
      apiVersion: '2016-04-18',
    });

    this.$sagemaker = new AWS.SageMaker({
      apiVersion: '2017-07-24',
    });
  }

  sanityCheck(data) {
    const missing = [
      'SolutionId',
      'UserPool',
      'UserGroup',
      'AppClientId',
      'TopicArn',
      'UserPoolDomain',
    ].filter(x => data[x] === undefined);
    if (missing.length) {
      throw new Error(`missing ${missing.join(', ')}`);
    }
  }

  get data() {
    return this.$data;
  }

  get solutionId() {
    return this.data.SolutionId;
  }

  get userPool() {
    return this.data.UserPool;
  }

  get userGroup() {
    return this.data.UserGroup;
  }

  get clientId() {
    return this.data.AppClientId;
  }

  get topicArn() {
    return this.data.TopicArn;
  }

  get userPoolDomain() {
    return this.data.UserPoolDomain;
  }

  get workteamName() {
    return `${this.userPoolDomain}-team`;
  }

  get cognito() {
    return this.$cognito;
  }

  get sagemaker() {
    return this.$sagemaker;
  }

  normalize(name) {
    return name.replace(/[^a-zA-Z0-9-]/g, '-');
  }

  async preconfigure() {
   
    await this.cognito.createUserPoolDomain({
      Domain: this.userPoolDomain,
      UserPoolId: this.userPool,
    }).promise();

    
    await this.cognito.updateUserPoolClient({
      ClientId: this.clientId,
      UserPoolId: this.userPool,
      AllowedOAuthFlows: [
        'code',
        'implicit',
      ],
      AllowedOAuthFlowsUserPoolClient: true,
      AllowedOAuthScopes: [
        'email',
        'openid',
        'profile',
      ],
      ExplicitAuthFlows: [
        'USER_PASSWORD_AUTH',
      ],
    
      CallbackURLs: [
        'https://127.0.0.1',
      ],
      LogoutURLs: [
        'https://127.0.0.1',
      ],
      SupportedIdentityProviders: [
        'COGNITO',
      ],
    }).promise();
  }

  async queryCurrentTeam() {
    const {
      Workteams,
    } = await this.sagemaker.listWorkteams({
      MaxResults: 100,
    }).promise();

    if (!Workteams.length) {
      return undefined;
    }

    const team = Workteams.shift();
    if (!team.MemberDefinitions || !team.MemberDefinitions.length) {
      return undefined;
    }

    const {
      CognitoMemberDefinition,
    } = team.MemberDefinitions.shift();

    return {
      UserPool: CognitoMemberDefinition.UserPool,
      ClientId: CognitoMemberDefinition.ClientId,
    };
  }

  async cognitoCreateGroup(userPool) {
    if (!userPool) {
      throw new Error('cognitoCreateGroup - userPool is null');
    }

    return this.cognito.createGroup({
      GroupName: this.userGroup,
      Description: `${this.solutionId} labeling workteam user group`,
      UserPoolId: userPool,
    }).promise();
  }

  async createTeam(current = {}) {
 
    if (current.UserPool) {
      await this.cognitoCreateGroup(current.UserPool);
    }

    const params = {
      Description: `(${this.solutionId}) labeling workteam`,
      MemberDefinitions: [{
        CognitoMemberDefinition: {
          UserPool: current.UserPool || this.userPool,
          ClientId: current.ClientId || this.clientId,
          UserGroup: this.userGroup,
        },
      }],
      WorkteamName: this.workteamName,
      NotificationConfiguration: {
        NotificationTopicArn: this.topicArn,
      },
      Tags: [
        {
          Key: 'SolutionId',
          Value: this.solutionId,
        },
      ],
    };

    await this.sagemaker.createWorkteam(params).promise();

    const {
      Workteam,
    } = await this.sagemaker.describeWorkteam({
      WorkteamName: this.workteamName,
    }).promise();

    return Workteam;
  }

  async postconfigure(team = {}) {
    if (!team.SubDomain) {
      throw new Error('postconfigure - SubDomain is null');
    }

    let template = PATH.join(PATH.dirname(__filename), 'fixtures/email.template');
    template = FS.readFileSync(template);
    template = template.toString().replace(/%URI%/g, `https://${team.SubDomain}`);

    await this.cognito.updateUserPool({
      UserPoolId: this.userPool,
      AdminCreateUserConfig: {
        AllowAdminCreateUserOnly: true,
        InviteMessageTemplate: {
          EmailMessage: template,
          EmailSubject: `You are invited by ${this.workteamName} to work on a labeling project.`,
        },
      },
    }).promise();
  }

  async createResource() {
    await this.preconfigure();
    const current = await this.queryCurrentTeam();
    const team = await this.createTeam(current);
    await this.postconfigure(team);

    this.storeResponseData('UserPool', (current && current.UserPool) || this.userPool);
    this.storeResponseData('ClientId', (current && current.ClientId) || this.clientId);
    this.storeResponseData('UserGroup', this.userGroup);
    this.storeResponseData('TeamName', this.workteamName);
    this.storeResponseData('TeamArn', team.WorkteamArn);
    this.storeResponseData('Status', 'SUCCESS');

    return this.responseData;
  }

  async deleteResource() {
    try {
      const {
        Workteam,
      } = await this.sagemaker.describeWorkteam({
        WorkteamName: this.workteamName,
      }).promise();

      /* delete workteam only if it exists */
      if ((Workteam || {}).WorkteamArn) {
        const {
          Success,
        } = await this.sagemaker.deleteWorkteam({
          WorkteamName: this.workteamName,
        }).promise();

        if (!Success) {
          throw new Error(`failed to delete Workteam, ${this.workteamName}`);
        }
      }

      /* delete user group */
      if ((Workteam || {}).MemberDefinitions) {
        const {
          UserGroup,
          UserPool,
        } = (Workteam.MemberDefinitions.shift() || {}).CognitoMemberDefinition || {};

       
        if (UserGroup && UserPool && this.userPool !== UserPool) {
          await this.cognito.deleteGroup({
            GroupName: UserGroup,
            UserPoolId: UserPool,
          }).promise();
        }
      }
    } catch (e) {
      console.error(e);
    }

    try {
      const response = await this.cognito.describeUserPoolDomain({
        Domain: this.userPoolDomain,
      }).promise();

      /* delete domain only if it exists */
      if (((response || {}).DomainDescription || {}).Domain) {
        await this.cognito.deleteUserPoolDomain({
          Domain: this.userPoolDomain,
          UserPoolId: this.userPool,
        }).promise();
      }
    } catch (e) {
      console.error(e);
    }

    this.storeResponseData('Status', 'DELETED');

    return this.responseData;
  }

  async updateResource() {
    await this.deleteResource();
    return this.createResource();
  }

  async configure() {
    if (this.isRequestType('Delete')) {
      return this.deleteResource();
    }

    if (this.isRequestType('Update')) {
      return this.updateResource();
    }

    return this.createResource();
  }
}

module.exports = PrivateWorkforce; 

package.json

{
  "$schema": "http://json.schemastore.org/package",
  "name": "custom-resources",
  "version": "1.0.0",
  "description": "(Custom Brand Detection) AWS CloudFormation Custom Resource Lambda function",
  "main": "index.js",
  "private": true,
  "scripts": {
    "pretest": "npm install",
    "test": "mocha *.spec.js",
    "build:clean": "rm -rf dist && mkdir -p dist",
    "build:copy": "cp -rv index.js package.json lib dist/",
    "build:install": "cd dist && npm install --production",
    "build": "npm-run-all -s build:clean build:copy build:install",
    "zip": "cd dist && zip -rq"
  },
  "author": "aws-specialist-sa-emea",
  "license": "MIT-0",
  "dependencies": {
    "adm-zip": "^0.4.14",
    "mime": "^2.4.5",
    "@aws-sdk/client-sagemaker": "^3.0.0"
  },
  "devDependencies": {
    "core-lib": "file:../layers/core-lib"
  }
}
No Answers

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions

Relevant content