[AWS] EC2: 불사조 인스턴스 만들기

EC2로 이런저런 작업을 하되, 뭔가 문제가 생길때마다 인스턴스를 재부팅해야 할 경우가 많다.
원체 EC2 인스턴스가 잘 죽기도 하고... 관리형 서비스가 아니다보니 이런저런 일이 생길때가 많기 때문이다.

작업환경은 Node.js다.
다른 언어들로도 가능하지만, node가 자료도 좀 더 풍부하고 나은 편인 것 같다.




단순 재부팅

일단, EC2에 대한 제어는 기본적으로 인스턴스 ID를 통해서 처리할 수 있다.

저기 있는 저 이상한 문자+숫자열이다.

저걸 갖다가 이런식으로 호출해주면 된다.

const AWS = require('aws-sdk');

const ec2 = new AWS.EC2();

...
// 재부팅!
await ec2.rebootInstances({
    InstanceIds: [
         "..." //여기에 인스턴스 ID
    ],        
}).promise();

그러면 직접 인스턴스 재부팅을 시켜주는 것과 동일하게 동작할 것이다.

보통 이런경우에 재부팅을 시켜주는 주체는 다른 인스턴스로 두는 경우가 많다.
A 인스턴스에 뭔가 이상이 생긴다면, B 인스턴스에서 그걸 감지해서 A 인스턴스를 재부팅시켜주는 형태다.




완전 재부팅 (중단 후 시작)

근데 가끔 그냥 리부트로도 해결되지 않는 경우가 있을 수도 있다.
IP나 하드웨어적 문제 같은 경우는 아예 물리적 컴퓨터부터가 바뀌어야 하기 때문이다.

그럴 때는 이런식으로 아예 인스턴스를 중단시킨 다음에 시작을 해줘야 한다.

const AWS = require('aws-sdk');

const ec2 = new AWS.EC2();

async function restart(instanceId) {
    // 인스턴스 중단
    await ec2.stopInstances({
        InstanceIds: [
             instanceId
        ],

    }).promise();

    // 완전히 중단될떄까지 대기
    await ec2.waitFor('instanceStopped', {
        InstanceIds: [
             instanceId
        ]
    }).promise();

    // 인스턴스 재시작
    await ec2.startInstances({
        InstanceIds: [
            instanceId
        ]
    }).promise();
}

await restart(instanceId);

이러면 하드웨어적 문제도 리셋되고, IP도 바뀌면서 재시작된다.




나의 사용사례

내 경우에는 인스턴스로 프로세스를 돌리되, 그 프로세스에서 필요하다고 판단되면 재실행시켜서 IP를 재할당시킬 필요가 있었다.

그래서 일단 재실행을 시켜주는 녀석을 람다로 작성해뒀다.
관리가 편하고, 호출이 잦지 않다면 훨씬 저렴할 것이기 때문이었다.

소스는 아래와 같이 짰었다.
인스턴스에게서 자신의 인스턴스ID를 전달받아 껐다 켜주는 형태다.

const AWS = require('aws-sdk');

const ec2 = new AWS.EC2();

async function restart(instanceId) {
    // 인스턴스 중단
    await ec2.stopInstances({
        InstanceIds: [
             instanceId
        ],

    }).promise();

    // 완전히 중단될떄까지 대기
    await ec2.waitFor('instanceStopped', {
        InstanceIds: [
             instanceId
        ]
    }).promise();

    // 인스턴스 재시작
    await ec2.startInstances({
        InstanceIds: [
            instanceId
        ]
    }).promise();
}

exports.handler = async (event) => {
    // 매개변수로 인스턴스 ID 받음
    let instanceId = event['instanceId'] ?? null;
    console.log(event)

    if(instanceId) {
        instanceId = instanceId.replace('\n', '');

        await restart(instanceId);
    }

    const response = {
        statusCode: 200,
        body: JSON.stringify('SUCCESS'),
    };
    return response;
};

켜줘야 할놈이 좀 많고, 유동적으로 생겼다 없어질 수 있기 때문에 유연하게 동작할 수 있도록 구성했다.
그리고 이건 어차피 외부에서는 호출이 불가능해서 보안적으로도 큰 문제는 없다고 판단했다.

실행시간은 조금 늘여놨다.
인스턴스가 중단되는데 한 20-30초는 걸리기 때문에 그걸 기다리기 위함이다.

람다에 EC2 제어 권한도 달아준다.

그리고 재부팅이 필요한 인스턴스에서는 다음과 같이 호출했다.

import aws from "aws-sdk";
import { readFileSync } from "fs";

async function restartEc2() {
    aws.config.update({
        region: "ap-northeast-2",
        accessKeyId: ...,
        secretAccessKey: ...,
    });

    // 자신의 인스턴스ID 획득 (아마존 리눅스 기준)
    const instanceId = String(readFileSync("/var/lib/cloud/data/instance-id"));

    const lambda = new aws.Lambda();

    await lambda
        .invoke({
            FunctionName: "재부팅용 함수명",
            Payload: JSON.stringify({
                instanceId,
            }),
        })
        .promise();
}

...
await restartEc2();

그래서 그냥 이런 구조다.
관리도 편하고 별거없다.

아직까진 만족스럽게 사용하고 있다.




그외

나는 아주 단순하게 인스턴스 자신이 소생요청을 하는 식으로 구현을 했지만, 일반적으로 이런 구성이 필요할 경우는 많지 않을 것이다.

보통 재부팅은 컴퓨터나 뭐 프로세스가 몽땅 죽을 때 필요한 건데, 이미 죽은놈이 어떻게 살려달라는 요청을 하겠는가.

그래서 보통 상태를 감시하는 인스턴스같은걸 띄워놓고 죽으면 재깍재깍 리부트해주는 식으로 많이 한다고 한다.

단점은 감시용 인스턴스가 죽을 경우에는 방법이 없다는 것인데...

여기에 대한 가장 괜찮아보이는 해결책은, 같은 동작을 하는 인스턴스 3개 이상 띄워놓고 서로서로 감시해서 재부팅해주는 것이었다.
필요한 작업도 똑같이 하면서 서로서로 살려주는 역할도 하니, 웬만하면 다 죽을 일이 없다.
전설의 피닉스 데몬이라고 부르더라.



참조
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html