AWSElasticsearch (1)

회사에서 AWS Elasticsearch를 이용하여 상용서비스의 로그를 기록하고 있는데 그 용량이 좀 많아 10TB용량으로 1달 정도밖에 버티고 있지 못해 이런 저런 리서치를 좀 하다가 해당 데이터를 S3로 백업해보기로 하였고 그 과정을 기록해본다.

 

 

참고로 AWS SDK는 1.11.483 사용중이며, role 기반으로 사용하고 있어 access_key, secret_key등은 사용하지 않는다.

 

우선 아래 라이브러리를 추가해주었다.

// ES snapshot S3 저장을 위한 디펜던시 추가
compile 'org.elasticsearch:elasticsearch:7.1.1'
compile 'org.elasticsearch.client:elasticsearch-rest-high-level-client:7.1.1'

 

1. 스냅샷 리포지토리 등록

a. Kibana에서 직접 등록

PUT _snapshot/my-snapshot-repo
{
  "type": "s3",
  "settings": {
    "bucket": "버킷명, 예제에서는 my-bucket 사용",
    "base_path": "/로 시작하지 않는 폴더경로, 버킷 내 폴더 경로임, 예제에서는 base/path/to/save/backup/data 사용",
    "readonly" : "true",
    "region": "us-west-1",
    "role_arn": "arn:aws:iam::4234:my-role",
    "compress" : "true"
  }
}

만약 위 명령어 실행시 아래와 같이 오류가 발생한다면 role 설정에 추가해야할 사항이 있는데, 해당 포스트 최하단의 참고문헌들을 읽어보며 해결해보시길 바란다. 

{
  "Message": "User: anonymous is not authorized to perform: iam:PassRole on resource: arn:aws:iam::4234:my-role"
}

 

나는 아래 설명할 Java 코드를 이용하여 등록했다.

 

b. Java 코드 (1과 동일한 내용을 자바코드로 작성한 것이다)

    import com.amazonaws.auth.AWS4Signer;
    import com.amazonaws.auth.AWSCredentialsProvider;
    import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.http.HttpEntity;
    import org.apache.http.HttpHost;
    import org.apache.http.HttpRequestInterceptor;
    import org.apache.http.entity.ContentType;
    import org.apache.http.nio.entity.NStringEntity;
    import org.elasticsearch.action.get.GetRequest;
    import org.elasticsearch.action.get.GetResponse;
    import org.elasticsearch.client.*;
    import org.junit.Ignore;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.ActiveProfiles;
    import org.springframework.test.context.junit4.SpringRunner;

    import javax.ws.rs.HttpMethod;
    import java.io.IOException;

    @Slf4j
    @ActiveProfiles("${SPRING_PROFILES_ACTIVE:local}")
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class AWSESSnapshotTest {

        private String region = "us-west-1";
        private static String serviceName = "es";
        private static String aesEndpoint = "VPC Endpoint를 넣어주세요";
        private static String snapshotRepository = "/_snapshot/리포지토리명을 써주세요";
        private static String snapshotSettings = "{ \"type\": \"s3\", \"settings\": { \"bucket\": \"버킷명을 써주세요\", \"region\": \"리전을 명시해주세요, 예: us-west-1\", \"base_path\": \"스냅샷을 저장할 버킷내 폴더 경로\", "compress":"true", "readonly":"true", \"role_arn\": \"IAM Role을 적어주세요\" } }";

        private static final AWSCredentialsProvider credentialsProvider = new DefaultAWSCredentialsProviderChain();

        @Test
        public void AWS_ES_수동스냅샷_리포지토리_등록() throws IOException {
            RestClient esClient = esClient(serviceName, region);

            // Register a snapshot repository
            HttpEntity entity = new NStringEntity(snapshotSettings, ContentType.APPLICATION_JSON);
            Request request = new Request(HttpMethod.PUT, snapshotRepository);
            request.setEntity(entity);
            // request.addParameter(name, value); // optional parameters
            Response response = esClient.performRequest(request);
            System.out.println(response.toString());
        }

        // Adds the interceptor to the ES REST client
        public static RestHighLevelClient esClient2(String serviceName, String region) {
            AWS4Signer signer = new AWS4Signer();
            signer.setServiceName(serviceName);
            signer.setRegionName(region);

            // java.lang.NoClassDefFoundError: org/elasticsearch/common/xcontent/DeprecationHandler
            HttpRequestInterceptor interceptor = new AWSRequestSigningApacheInterceptor(serviceName, signer, credentialsProvider);

            return new RestHighLevelClient(RestClient.builder(HttpHost.create(aesEndpoint)).setHttpClientConfigCallback(hacb -> hacb.addInterceptorLast(interceptor)));
        }

    }

 

자바코드가 정상적으로 실행되면 ES에서 아래 명령어로 설정이 잘 생성되었는지 확인해보자.

GET _snapshot

"my-repo" : {
  "type" : "s3",
  "settings" : {
    "bucket" : "my-bucket",
    "base_path" : "base/path/to/save/backup/data",
    "readonly" : "false",
    "region" : "us-west-1",
    "role_arn" : "arn:aws:iam::4234:my-role",
    "compress" : "true"
  }
}

 

이렇게 잘 나왔다면 이제 인덱스 백업으로 넘어가자.

 

2. ES 인덱스를 S3로 백업

ES 인덱스를 S3로 백업할 때는 아래와 같이 할 수 있다.

    public void AWS_ES_수동스냅샷_S3_저장() throws IOException {

        RestClient esClient = esClient(serviceName, region);

        // Save indexes into S3 repository
        String takeSnapShot = "{\n  \"indices\": \"index-2020-04-*\",\n  \"ignore_unavailable\": true,\n  \"include_global_state\": false\n}";
        HttpEntity entity = new NStringEntity(takeSnapShot, ContentType.APPLICATION_JSON);
        Request request = new Request(HttpMethod.PUT, snapshotRepository + "/snapshot-test-2020-04");
        request.setEntity(entity);
        
        Response response = esClient.performRequest(request);
        
        System.out.println(response.toString());
    }
    
    
    출력결과 : 
    Response{requestLine=PUT /_snapshot/my-repo/snapshot-test-2020-04 HTTP/1.1, host=${VPC Endpoint}, response=HTTP/1.1 200 OK}

 

위 코드는 인덱스명이 "index-2020-04-"로 시작하는 모든 인덱스를 "snapshot-test-2020-04"라는 이름의 스냅샷으로 S3에 저장하고 있다.

그런데 실제로 S3의 해당 리포지토리로 가서 확인해보면 indices 디텍토리가 생성되어있고, 그 안에는 백업하려했던 인덱스의 개수만큼 uuid같은 이름의 디렉토리가 생성되어 있고 그 디렉토리 안에는 또 uuid같은 이름의 파일들이 잔뜩 생성되어있는 것을 확인 할 수 있었다. 따라서 S3 파일명만 보고서는 이게 어느 스냅샷에 대한 데이터파일인지 알 수가 없다 아놔 ㅜㅜ 추후 S3에서 해당 스냅샷에 대한 데이터를 삭제하고 싶어도 여기서(S3에서) 파일명만 보고는 처리할 수가 없다는 뜻이다. 

 

우선 백업된 수동 스냅샷을 다시 ES로 복원해서 잘 조회해올 수 있는지부터 확인해보자.

 

3. S3에 백업된 스냅샷을 ES로 복원하기

복원시에는 아래처럼 할 수 있다.

    public void AWS_ES_수동스냅샷_S3_복원_테스트() throws IOException {

        RestClient esClient = esClient(serviceName, region);

        // Restoring snapshot as ES indices
        String takeSnapShot = "{\n  \"indices\": \"index-2020-04-28\",\n  \"ignore_unavailable\": true,\n  \"include_global_state\": false\n}";
        HttpEntity entity = new NStringEntity(takeSnapShot, ContentType.APPLICATION_JSON);
        Request request = new Request(HttpMethod.POST, snapshotRepository + "/snapshot-test-2020-04/_restore");
        request.setEntity(entity);
        
        Response response = esClient.performRequest(request);
        
        System.out.println(response.toString());
    }
    
    출력결과:
    Response{requestLine=POST /_snapshot/my-repo/snapshot-test-2020-04/_restore HTTP/1.1, host=${VPC Endpoint}, response=HTTP/1.1 200 OK}

위 코드는 4월달 로그에 대해서 저장했던 스냅샷에서 특정일자의 인덱스 즉, index-2020-04-28 에 대해서만 복원을 진행한다.

위 코드가 정상적으로 실행되면 ES에 해당 인덱스가 생성되어 있는 것을 확인할 수 있다.

만약 복원하려는 인덱스 명이 존재한다면 아래와 같이 오류가 발생한다.

org.elasticsearch.client.ResponseException: method [POST], host [https://vpc-endpoint.amazonaws.com], URI [/_snapshot/my-repo/snapshot-test-2020-04/_restore], status line [HTTP/1.1 500 Server Error]
{"error":{"root_cause":[{"type":"snapshot_restore_exception","reason":"[my-repo:snapshot-test-2020-04/ECzLylZnTsGn8KfBqCvSEw] cannot restore index [index-2020-04-27] because an open index with same name already exists in the cluster. Either close or delete the existing index or restore the index under a different name by providing a rename pattern and replacement name"}],"type":"snapshot_restore_exception","reason":"[my-repo:snapshot-test-2020-04/ECzLylZnTsGn8KfBqCvSEw] cannot restore index [index-2020-04-27] because an open index with same name already exists in the cluster. Either close or delete the existing index or restore the index under a different name by providing a rename pattern and replacement name"},"status":500}

 

이럴 경우 아래 3가지 경우로 복원절차를 진행해야 한다.

 

첫째, 현재 존재하는 동일한 이름의 인덱스를 삭제하고 복원한다.

둘째, 복원할 때 인덱스명을 rename해서 복원한다.

셋째, 다른 ES도메인으로 복원한다 -> 이건 수동으로 스냅샷을 생성한 경우에만 가능한 방법이다.

 

나는 테스트 중이므로 간편하게 첫번째를 선택했다.

 

만약 스냅샷에 여러 인덱스에 대한 정보가 담겨있고 모든 정보를 복원하려고 할 때 단 하나의 인덱스라도 이름이 겹친다면 위 에러가 발생한다.

 

위 코드가 정상적으로 실행되면 ES에서 해당 인덱스의 정보를 조회해올 수 있다.

 

그리고 특정 인덱스가 아닌 여러 인덱스를 한꺼번에 복원하고자 할 때는 takeSnapShot의 indices 항목에 인덱스명을 나열해주면 된다. *도 사용할 수 있다는 건 ES 유저라면 당연히 아실테고..

"indices":"index-2020-04-*,index-2020-05-0*"

위처럼 해주면 4월달 모든 인덱스와 5월 1일~5월 9일까지의 인덱스를 복원하게 될 것이다.

 

자, 그럼 복원에 대해서는 이정도로 하고, 스냅샷도 너무 오래 보관하면 용량만 잡아먹을테니 삭제는 어떻게 할 수 있을지 보자.

 

4. S3에 백업한 ES 스냅샷 삭제하기

스냅샷을 삭제할 때는 아래 코드를 참고하면된다.

    public void AWS_ES_수동스냅샷_삭제_테스트() throws IOException {

        RestClient esClient = esClient(serviceName, region);

        Request request = new Request(HttpMethod.DELETE, snapshotRepository + "/snapshot-test-2020-04");

        Response response = esClient.performRequest(request);

        System.out.println(response.toString());
    }

이렇게 하면 S3에 생성되었던 파일들이 삭제되는 것을 확인할 수 있는데, indices 디렉토리는 완전히 삭제되고 스냅샷 리포지토리로 지정된 root 경로에는 몇몇 쓰레기(??) 파일들이 남아있는 것을 확인 할 수 있었다. 만약 2개 이상의 스냅샷을 저장했다면 indices 디렉토리는 모든 스냅샷에 대한 삭제요청을 하지 않는 이상 삭제되지 않는다. 또한, 스냅샷을 삭제할 때는 시간이 오래걸린다. sync 방식으로 처리하기에는 너무너무너무 오래 걸리기 때문에 상용에서 삭제기능을 사용하려면 async 방식으로 처리하길 권장한다. 위 코드대로 작성했다면 무조건 아래 소켓타임아웃 예외가 발생할 것이다.

java.net.SocketTimeoutException: 30,000 milliseconds timeout on connection http-outgoing-0 [ACTIVE]

 

만약 스냅샷 이름이 기억이 나지 않는다면 어떻게 해야 할까? S3에서 조회해봐야 파일명이 어떤 스냅샷인지 알 수 없도록 생성되어있으니 알 길이 없다. 키바나 dev tool에서 아래 명령어를 이용하여 어떤 스냅샷이 있는지 그 스냅샷에 어떤 인덱스들이 저장되어있는지를 확인할 수 있다.

GET _snapshot/my-repo/_all 	<- my-repo의 모든 스냅샷 조회하기



{
  "snapshots" : [ {
    "snapshot" : "snapshot-test-2020-04",
    "uuid" : "VPMGlnLTQlqIT7SxPaqCOg",
    "version_id" : 7016199,
    "version" : "7.1.1",
    "indices" : [ "index-2020-04-29", "index-2020-04-26", "index-2020-04-27", "index-2020-04-28" ],
    "include_global_state" : false,
    "state" : "SUCCESS",
    "start_time" : "2020-05-25T07:34:09.804Z",
    "start_time_in_millis" : 1590392049804,
    "end_time" : "2020-05-25T07:34:32.145Z",
    "end_time_in_millis" : 1590392072145,
    "duration_in_millis" : 22341,
    "failures" : [ ],
    "shards" : {
      "total" : 20,
      "failed" : 0,
      "successful" : 20
    }
  } ]
}

 

특정 스냅샷에 대한 정보를 확인하고 싶다면 아래 명령어를 이용하면 된다.

GET _snapshot/my-repo/snapshot-test-*

 

 

참고로 큐레이터를 이용하여 ES의 데이터를 rotate 시킬 수도 있습니다. 관련 링크는 참고문서에 넣어놓았으니 관심있으시면 읽어보세요.

 

그리고 실제로는 이 스냅샷 코드를 사용하지 않고 ES 7 이상에서 지원하는 UltraWarm 설정을 이용하기로 했습니다. ES인스턴스 사이즈도 warm 노드의 경우 핫노드와는 별개로 S3를 이용함으로써 비용을 많이 줄일 수 있고 저장기간 역시 원하는 만큼 늘릴 수 있었기 때문에 현재 상황에서 선택할 수 있는 최선이었습니다. UltraWarm이 뭔지에 대해서는 참고문서의 링크를 확인해보세요. 참고로 ES 버전 6.8이상에서만 사용가능합니다.

 

UltraWarm 으로 마이그레이션 할 때는 아래 명령어들을 참고하면 됩니다.

 

// 마이그레이션 작업 요청
POST _ultrawarm/migration/index-2020-02-09/_warm

 

// 마이그레이션 작업 중인 인덱스의 상태 확인
GET _ultrawarm/migration/index-2020-02-09/_status

-> 마이그레이션 작업중이 아닌경우 즉, 끝났거나 시작도 안한 상태면 오류메시지가 출력됩니다.

 

// 인덱스 패턴 목록 조회
GET _cat/indices/index-2020-02-0*?v&s=index


-> 이 명령은 마이그레이션 작업 진행 중에 인덱스의 사이즈가 어떻게 변하는지 확인하기 위해 사용했었다. 마이그레이션 작업이 시작되면 마이그레이션하려는 인덱스의 원래 사이즈보다 2배까지 hot data node의 용량을 차지하게된다. 여유분의 사이즈가 없다면 마이그레이션 작업 자체가 시작되지 않는다. (free size를 검사한 뒤 시작하도록 되어있다)

 

// 마이그레이션이 완료된 인덱스 상태 조회
GET index-2020-02-09/_settings


-> Ultrawarm 노드로 마이그레이션이 완료되었다면 위 명령어로 조회 시 box_type이 warm 으로 바뀌어 있는 것을 확인할 수 있다.

 

 

참고문서

https://www.elastic.co/guide/en/elasticsearch/plugins/current/repository-s3-repository.html

https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-managedomains-snapshots.html#es-managedomains-snapshot-prerequisites

https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-request-signing.html#es-request-signing-java

https://aws.amazon.com/ko/blogs/database/use-amazon-s3-to-store-a-single-amazon-elasticsearch-service-index/

https://www.elastic.co/guide/en/elasticsearch/reference/7.1/modules-snapshots.html

AWS 큐레이터 이용하기

AWS ES Ultrawarm