js

Nestjs로 aws EC2 instance 제어하기

개발하는 가오나시 2023. 11. 27. 14:56

반복적으로 들어오는 이벤트의 환경을 세팅하는게 번거롭기에 자동으로 인스턴스와 이벤트를 관리해주는 기능 개발

어드민에서 서버스팩선택해서 서버생성 버튼 클릭시 인스턴스 생성 및 user-data 파일실행하여 웹서버 실행

초기 세팅

  • env
    app.module.ts
    
    import { ConfigModule } from '@nestjs/config' 
    
    @Module({
    	import: [ConfigModule.forRoot({ isGlobal: true})]
    })
    
    이렇게 import 해주면 프로젝트 루트 경로의 .env파일을 읽어와서 process.env를 통하여 사용할 수 있게된다. 추가적으로 모듈을 다른 모듈에서 사용하려고하면 사용하려는 모듈을 import해줘야하는데, isGlobal옵션으로 다른 모듈에서는 Import없이 사용할 수 있게된다.
  • nestJS에서 env를 사용할 수 있게 nestjs/config 라이브러리를 설치하여 app.module에 import해준다.
  • DB - mongo
    app.module.ts
    
    @Module({
    	import: [
    		ConfigModule.forRoot({ isGlobal: true}),
    		MongooseModule.forRootAsync({
    			imports:[ConfigModule] // 위에있는 config모듈을 가져와서
    				useFactory: async (config:ConfigService)=>({
    					uri: conf.get<string>('MONGODB_URL'),
    					user: conf.get<string>('MONGODB_USER'),
    					pass: conf.get<string>('MONGODB_pass'),
    				}),
    			inject: [ConfigService],
    		})
    	]
    })
    이렇게 mongoose를 세팅해주면, 우리가 생성하는 모듈별로 collection을 만들어준다.
  • nest에 mongoDB를 사용할 려고 하니 nestjs 공홈에서 mongoose를 활용하라 추천해준다. 추천해주는대로 mongoose 와 nestjs/mongoose를 설치해준다. db정보는 .env에 숨겨두었으니 env에 있는 정보로 db연결을 해줘야한다.
  • AWS
    app.module.ts
    
    @Module({
    	import: [
    		ConfigModule.forRoot({ isGlobal: true}),
    		MongooseModule.forRootAsync({
    			imports:[ConfigModule] // 위에있는 config모듈을 가져와서
    			useFactory: async (config:ConfigService)=>({
    				uri: conf.get<string>('MONGODB_URL'),
    				user: conf.get<string>('MONGODB_USER'),
    				pass: conf.get<string>('MONGODB_pass'),
    			}),
    			inject: [ConfigService],
    		}),
    		AwsSdkModule.forRoorAsync({
    			defaultServiceOptions: {
    				useFactory: (cs: ConfigService) => {
    					return {
    						region: 'ap-northeast-2',
    						credentials: {
    							accessKeyId: cs.get<string>('AWS_ACCESS_KEY'),
    							secretAccessKey: cs.get<string>('AWS_SEC_ACCESS_KEY'),
    						},
    					};
    				},
    				inject: [ConfigService],
    			},
    		})
    	]
    })

    이렇게 기본적인 준비가 끝났다. 이제 인스턴스 생성부터 시작하자.
    1. 자동으로 인스턴스를 띄워서
    2. 내부의 서버를 띄우고
    3. 상황에 따라 이벤트를 띄우는 것이다.
    이제 1번부터 구분 동작으로 들어가기 전에, 알아야하는 것이 있다. aws의 ec2 instance가 실행되면서 동작하는 스크립트가 있다. 이를 user-data라고 하며, 우리의 2번 목표를 위해 이를 알아야한다. 필자는 인스턴스가 (1)start 상태가 될 때마다 서버를 실행하고 완료됨을 알려주기 위해서 (2)api로 준비완료된 인스턴스의 인스턴스 아이디를 api로 받을 것이다.
    instance_init.sh
    
    Content-Type: multipart/mixed; boundary="//"
    MIME-Version: 1.0
    
    --//
    Content-Type: text/cloud-config; charset="us-ascii"
    MIME-Version: 1.0
    Content-Transfer-Encoding: 7bit
    Content-Disposition: attachment; filename="cloud-config.txt"
    
    #cloud-config
    cloud_final_modules:
    - [scripts-user, always]
    
    --//
    Content-Type: text/x-shellscript; charset="us-ascii"
    MIME-Version: 1.0
    Content-Transfer-Encoding: 7bit
    Content-Disposition: attachment; filename="userdata.txt"
    -------------------------- 여기까지가 인스턴스 start될 때마다 실행되게하는 스크립트
    #!/bin/bash 
    /home/ec2-user/.nvm/versions/node/v20.9.0/bin/npm install -g pm2
    # 인스턴스 실행시 환경변수가 제대로 적용되지않았을 수 있어서 직접 경로를 입력하여 사용한다.
    
    export PATH=$PATH:/home/ec2-user/.nvm/versions/node/v20.9.0/bin
    sudo env PATH=$PATH:/usr/bin /home/ec2-user/.nvm/versions/node/v20.9.0/bin/pm2 startup systemd -u ec2-user --hp /home/ec2-user
    
    instanceId=$(cat /var/lib/cloud/data/instance-id)
    # ec2 instance는 본인의 인스턴스 아이디를 가지고 있다. 이를 가져와서 변수에 담는다.
    
    curl -X POST -H "Content-Type:application/json"  api-url-넣으시고 --data '{"instanceId":"'"${instanceId}"'"}'
    # 필자는 인스턴스가 실행되면, pm2로 서버를 띄우고, 실행된 인스턴스id를 api로 받았다.
    

이렇게 실행 스크립트를 작성했으니 인스턴스를 만들어보자. instance모듈을 만든다.

src/instance/instance.module.ts

@Module({
	imports: [
		MongooseModule.forFeature([]),
		AwsSdkModule.forFeatures([EC2])
	],
	controllers: [],
	providers: []
})

아까 mongoose가 자동으로 만들어주는 collection을 사용하기위해서 MongooseModule을 사용하는데, 이때 스키마를 먼저 선언해줘야한다.

src/instance/schema/instance.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import * as mongoosePaginate from 'mongoose-paginate-v2';
import * as mongoose from 'mongoose';

@Schema({
  timestamps: { createdAt: 'created', updatedAt: 'updated' },
})
export class Instance {
  @Prop({ required: false, type: mongoose.Schema.Types.String })
  ip: string;

  @Prop({ required: true, type: mongoose.Schema.Types.String })
  spec: string;

  @Prop({ required: false, type: mongoose.Schema.Types.String })
  status: string;

  @Prop({ required: false, type: mongoose.Schema.Types.String })
  instanceId: string;
}

export type InstanceDocument = mongoose.HydratedDocument<Instance>;

const schema = SchemaFactory.createForClass(Instance);
schema.plugin(mongoosePaginate);

export const InstanceSchema = schema;

이렇게 데이터 구조를 짜고

src/instance/instance.module.ts

import { Instance, InstanceSchema } from './schema/instance.schema';

@Module({
	imports: [
		MongooseModule.forFeature([
			{ name: Instance.name, schema: InstanceSchema } //추가해준다.
		]),
		AwsSdkModule.forFeatures([EC2])
	],
	controllers: [],
	providers: []
})

이제서야 준비가 진짜 끝났다.

src/instance/instance.service.ts

@Injectable()
export class InstanceService{
	constructor(
		@InjectModel(Instance.name)
		private InstanceModel: PaginateModel,
		@InjectAwsService(EC2) private readonly ec2: EC2,
	){}

	async create(createInstanceDto: CreateInstanceDto){
		const startScript = await readFileSync('src/instance_init.sh', {encoding: 'base64'}); 
		// 먼저 준비해둔 실행 스크립트 파일을 base64형태로 가져온다.
		const createEC2Props: EC2.Types.RunInstancesRequest = {
			ImageId: 미리-세팅해둔-ec2이미지,
			InstanceType: spec,
			KeyName: 키페어-이름,
			SecurityGroupIds: [보안-그룹-아이디],
			MinCount: 1,
			MaxCount: 1,
			SubnetId: 서브넷-아이디,
			TagSpecifications: [
				{
					ResourceType: 'instance',
					Tags: [{ Key: 'Name', Value: 인스턴스-이름 }],
				},
			],
			BlockDeviceMappings: [
				{
					DeviceName: '/dev/xvda',
					Ebs: {
					VolumeSize: 20, // 사용할 스토리지 사이즈
				},
			},
		],
		UserData: startScript, // 앞에서 가져온 실행스크립트
	};
	try{
		const createdEc2 = await this.ec2.runInstances(createEC2Props).promise();
		// 그냥 sdk의 메소드를 사용하면 aws객체가 나온다고 하니 promise를 받게 해주고
		const instanceId = createdEc2.Instances[0].InstanceId;
		// 생성한 인스턴스 id를 가져와서
		await new Promise((resolve) => setTimeout(resolve, 2000));
		// 2초정도 기다려주고			
		const getEC2 = await this.ec2
			.describeInstances({
				InstanceIds: [instanceId],
			})
			.promise();
		//인스턴스의 정보를 가져온다.
		const createDefault = {
			spec,
			eventName: '',
			ip: getEC2.Reservations[0].Instances[0].PublicIpAddress || '10.010.1',
			//해당 인스턴스의 public ip를 가져오고 
			status: 'booting',
			instanceId,
		};

		const instance = new this.InstanceModel(createDefault);
		await instance.save(); //mongoDB에 저장해준다.
		return true;
	}catch (err) {
			console.log(err);
			return false;
	}
}
}

이렇게 ip를 가져다두면, 해당 인스턴스에 떠있는 노드 서버로 api를 쏠 수 있게된다

 

인스턴스를 시작했으면 끄기도 해야한다.

async remove(id: string) {
    try {
      const objectId = new mongoose.Types.ObjectId(id);
      const data = await this.InstanceModel.findById(objectId);
      await this.ec2
        .stopInstances(
          {
            InstanceIds: [data.instanceId],
          },
          (err, data) => {
            if (err) {
              console.log(err);
              throw new Error('err');
            } else {
              console.log(data);
            }
          },
        )
        .promise();

      await this.InstanceModel.updateOne(
        {
          _id: objectId,
        },
        {
          status: 'stop',
          eventName: '',
          ip: '',
        },
      ).exec();
			//[mongoose도 그냥 메소드를 사용하면 유사 프로미스를 반환하니 제대로된 프로미스를 받기위해 exec를 쓴다](<https://tesseractjh.tistory.com/166>)
      return true;
    } catch (err) {
      console.log(err);
      return false;
    }
  }

'js' 카테고리의 다른 글

트위치 멀티 뷰어 개발기  (1) 2023.12.21
작업하던거 뒤로가기, 되돌리기 기능  (0) 2023.07.30
개발에도 온고지신이 필요하다.  (0) 2023.01.13
HTML string trim() 하기  (0) 2022.12.31
ajax 요청 보내기  (0) 2022.11.21