From 254c636a4d0b30239b6d711a561b4dfc6361c0e6 Mon Sep 17 00:00:00 2001 From: Yeonwu Oh Date: Wed, 3 Nov 2021 17:43:27 +0900 Subject: [PATCH] =?UTF-8?q?[#120]=20ML=20=EC=84=9C=EB=B2=84=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=20Interceptor=20=EA=B0=9C=EB=B0=9C=20=EB=B0=8F=20GET?= =?UTF-8?q?=20/auth/token=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /auth/token에 적용한 이유는 해당 API가 호출되었다면 회원가입 또는 로그인이 이루어졌으므로, 조금 후 분석이 일어날 수 있기 때문이다. 차후 Google Analytics와의 연동으로 접속자 수에 따라 ML 서버를 시작 / 종료하게 만들 계획이다. --- src/app.module.ts | 4 +- src/auth/auth.controller.ts | 10 +++- src/aws/aws-ml-instance.interceptor.ts | 82 ++++++++++++++++++++++++++ src/aws/aws.module.ts | 15 ++++- 4 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 src/aws/aws-ml-instance.interceptor.ts diff --git a/src/app.module.ts b/src/app.module.ts index 70a2e47..6fddc40 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -25,7 +25,7 @@ import { JwtMiddleware } from './auth/auth-jwt.middleware'; import { BookmarksModule } from './bookmarks/bookmarks.module'; import { AwsModule } from './aws/aws.module'; import { AwsSdkModule } from 'nest-aws-sdk'; -import { Lambda, S3, SharedIniFileCredentials } from 'aws-sdk'; +import { EC2, Lambda, S3, SharedIniFileCredentials } from 'aws-sdk'; const getEnvFilePath = (node_env: string): string => { if (node_env === 'dev') { @@ -115,7 +115,7 @@ const getEnvFilePath = (node_env: string): string => { timeout: 300000, }, }, - services: [S3, Lambda], + services: [S3, Lambda, EC2], }), MembersModule, PracticesModule, diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 47d3d71..184fdbb 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,4 +1,10 @@ -import { Controller, Get, Query, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Query, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ApiBadRequestResponse, @@ -9,6 +15,7 @@ import { ApiQuery, ApiTags, } from '@nestjs/swagger'; +import { StartMLInstanceInterceptor } from 'src/aws/aws-ml-instance.interceptor'; import { AuthJwt } from './auth-jwt.decorator'; import { AccessTokenGuard } from './auth.guard'; import { AuthService } from './auth.service'; @@ -72,6 +79,7 @@ export class AuthController { type: GetJwtOutput, }) @Get('token') + @UseInterceptors(StartMLInstanceInterceptor) getJwtToken(@Query() getJwtInput: GetJwtInput): Promise { return this.authService.getJwt(getJwtInput); } diff --git a/src/aws/aws-ml-instance.interceptor.ts b/src/aws/aws-ml-instance.interceptor.ts new file mode 100644 index 0000000..b5690ed --- /dev/null +++ b/src/aws/aws-ml-instance.interceptor.ts @@ -0,0 +1,82 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { EC2 } from 'aws-sdk'; +import { InjectAwsService } from 'nest-aws-sdk'; +import { Observable } from 'rxjs'; + +@Injectable() +export class StartMLInstanceInterceptor implements NestInterceptor { + static instanceId: string; + + constructor( + @InjectAwsService(EC2) + private readonly ec2Service: EC2, + private readonly config: ConfigService, + ) { + StartMLInstanceInterceptor.instanceId = + this.config.get('AWS_ML_INSTANCE_ID'); + } + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable | Promise> { + this.startInstance(); + return next.handle(); + } + + async startInstance() { + const instanceStatusResult = await this.ec2Service + .describeInstances({ + DryRun: false, + InstanceIds: [StartMLInstanceInterceptor.instanceId], + }) + .promise(); + + if (instanceStatusResult.$response.error) { + const error = instanceStatusResult.$response.error; + const errString = `${error.name}(${error.code}): ${error.message}`; + throw Error(errString); + } + + const mlInstance = instanceStatusResult.Reservations[0].Instances[0]; + const instanceState = mlInstance.State.Name; + + if (instanceState == 'stopping') { + this.startWhileStopping(); + } else if (instanceState == 'stopped') { + this.start(); + } else if ( + instanceState == 'terminated' || + instanceState == 'shutting-down' + ) { + throw new Error(`!!! MLInstance is ${instanceState}. !!!`); + } + } + + private async start() { + const result = await this.ec2Service + .startInstances({ + DryRun: false, + InstanceIds: [StartMLInstanceInterceptor.instanceId], + }) + .promise(); + return result.StartingInstances[0].InstanceId; + } + private async startWhileStopping() { + await this.ec2Service + .waitFor('instanceStopped', { + DryRun: false, + InstanceIds: [StartMLInstanceInterceptor.instanceId], + $waiter: { + delay: 3, + }, + }) + .promise(); + return await this.start(); + } +} diff --git a/src/aws/aws.module.ts b/src/aws/aws.module.ts index 26e10ac..c28179a 100644 --- a/src/aws/aws.module.ts +++ b/src/aws/aws.module.ts @@ -5,11 +5,22 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Analysis } from 'src/analyses/entities/analyses.entity'; import { UserImageS3Service } from './aws-user-image.service'; import { UserVideoS3Service } from './aws-user-video.service'; +import { StartMLInstanceInterceptor } from './aws-ml-instance.interceptor'; @Module({ imports: [TypeOrmModule.forFeature([Analysis])], - providers: [AwsService, UserVideoS3Service, UserImageS3Service], + providers: [ + AwsService, + UserVideoS3Service, + UserImageS3Service, + StartMLInstanceInterceptor, + ], controllers: [AwsController], - exports: [AwsService, UserVideoS3Service, UserImageS3Service], + exports: [ + AwsService, + UserVideoS3Service, + UserImageS3Service, + StartMLInstanceInterceptor, + ], }) export class AwsModule {}