슬랙에는 여러가지 방식으로 다른 서비스로부터의 외부 호출을 제공하는데, API Call을 통한 파일 업로드, 메세지 발송과, 심플하게 메시지 전송을 호출할 수 있는 WebHook 등을 제공하고 있다.

 

우리도 각종 알림용도로 여러 서비스에 연동해서 사용하는데, (메일 알림.. 장애 알림.. 등) 요청이 많지 않던 그 동안은 별 무리 없이 사용 하던 중 몇가지 제한사항이 발생했다.

 

Slack API 자체에서 일정 발송 횟수 이상이 넘어갈 경우 Too Many Request(429) 오류를 내뱉으며 잠시간 Block 상태가 되버려 요청들이 다 막히게 된다. 일반 메시지의 경우 여러개의 WebHook을 번갈아 가며 사용(WebHook 1개당 초당 1회 제한)하는 방식으로 어느정도 잦은 요청도 극복이 가능했지만, File upload의 경우 별도의 API로만 제공되어있어 제한을 피할 방법이 필요해졌다. 이에 429 오류 발생 시 일정 시간 이후 재시도 등의 처리로 극복하려 했으나, 일부 서비스에서 점점 더 잦은 요청으로 파일 메시지의 누락 등이 발생해서 다른 방안이 필요해졌다.

 

우선 Slack이 제공하는 File upload API의 호출 제한 사항을 보자

Slack files.upload API의 사양 (출처 : https://api.slack.com/methods/files.upload)

POST 방식으로 두가지 방식을 제공하는데, 실질적인 파일을 전송하는 방식은 multipart/form-data를 사용한다. (다른 방식의 경우 사이즈가 큰 본문이 파일처럼 전송된다.) 

 

여기서 문제가 되었던 중요 사항인 Rate limiting을 보면 Tier 2 로 되어있다.

 Tier 2와 다른 일부 Tier의 전송 제한(출처 : https://api.slack.com/docs/rate-limit)

20+ per minute로 되어있다. Note를 보면 1분당 20회, 약 3초당 1회 정도의 요청을 기본적으로 허용하고 일부 불가피하게 넘는 요청도 봐준다(?) 정도로 이해하면 된다.

 

우리의 서비스에서 해당 수치는 사실 파일 업로드를 사용하는 부서에서 전송하는 현황으로 생각하면 넉넉한 수치라고 생각했다. 다만 가끔 초당 수십회 이상 파일 업로드가 필요한 그런 경우가 생기게 되었고 이런 상황은 1분당 20회로는 하루에도 여러번 TooManyRequest(429) 와 맞닥뜨릴 수밖에 없는 상황이었다.

 

이에 처음에는 별도의 파일 서버를 두고 해당 파일의 링크만 담아, 전송 제한으로부터 훨씬 여유로운 WebHook을 이용하려 했다. 하지만 내부 논의 끝에 새로운 방식을 고안해냈다. (논의는 참가하지 않아서 선택기준은 자세히 전달 듣지 못했지만.. 아마 내부서비스이기도 하고 추가 파일서버의 필요함, 파일 전송 및 다운로드간 리소스 소모 등등의 이유였지 않을까 싶다.) 새로운 방식은 늦게 메시지를 받아보더라도 수신률은 최대로 유지하는 걸 기본 전략으로 삼아 Queue에 요청을 쌓아서 3초에 한개씩 전송하고, 오류나 실패시에도 다시 Queue에 적재하여 재시도하는 방식으로 신뢰성을 유지하는 방식을 선택하기로 했다.

 

이에, Queue 역할을 할 Redis와 알림 API 서비스 내부에 Scheduler를 구현하기로 하였다.

(Redis는 RabbitMQ 등으로 대체해도 된다.)

 

기존 방식

일반적인 API 호출 구조

기존에는 요청을 받으면 알림 서비스가 슬랙 API를 호출하는 간단한 구조였다.

 

새로운 방식

새로 Redis와 Scheduler를 적용한 방식

새로운 방식을 표현한 구조다. 기존 방식과 다르게 Queue역할을 해주는 Redis와, 주기적으로 Redis로부터 전송 데이터를 받아오는 Scheduler가 추가되어있다.

 

간단하게 플로우를 설명하면 다음과 같다.

 

1. 사용자가 파일을 알림 서비스로 업로드한다.

2. 알림 서비스는 파일을 받아 로컬 서버에 저장하고

3. 그 파일 정보와 전송정보를 Redis에 적재한다.

(여기까지가 사용자가 실질적으로 전송 요청을 마무리하는 단계)

4. 사용자 응답

5 알림 서비스의 스케쥴러가 3초마다 돌며 Redis에 파일 전송 정보가 있는지 체크하고 가져온다.

6. 전송 정보가 있을 경우 Slack File Upload API를 호출한다.

7. Slack으로부터 파일 업로드 결과를 받는다.

8. 오류가 발생하는 등, 전송에 실패하면 재시도를 위해 Redis에 다시 정보를 적재한다.

 

API의 기본 제한인 3초 1회 룰을 정확히 지키다 보니, 더이상 429 오류는 발생하지 않았다. 다만 약간의 burst한 요청을 Slack API가 허용하는 만큼, 파일 업로드 API를 집중 포화 할 일이 없는 부서가 해당 기능을 사용 할 때의 부득이한 지연을 방지하기 위해서 내부에서 429를 발생시키는 잦은 요청과, 일반적인 파일 업로드 요청을 두개의 API로 분리하기로 했다. 

 

결과적으로 일반 파일 업로드 요청과 지연 파일 업로드 요청으로 나뉘었는데, 실제 우리 서비스에 적용된 상세 플로우는 다음과 같다

2개로 나누어진 API의 상세 플로우

API : 알림 서비스의 전송 API
Scheduler : 알림 서비스의 일반 파일 업로드를 담당하는 스케쥴러
Scheduler-delayed : 알림 서비스의 지연 파일 업로드를 담당하는 스케쥴러
Sender : 알림 서비스에서 Slack 서버 API를 호출하는 주체
Redis server : Queue 역할을 담당하는 Redis

 

파일 관련 API가 두개로 나뉜 만큼 SchedulerRedisListKey도 두개로 늘어났다. (실제 서비스에는 일반 메세지를 담당하는 것 까지 3개씩 동작하고 있다)

 

지연 파일 업로드 뿐 아니라, 일반 파일 업로드 기능도 제공하는 만큼 3초당 1회 호출 룰이 약간 초과할 여지가 있기에 429가 발생할 여지 또한 남아있지만, 기존과 다르게 Queue형식을 채용함으로서 429가 발생하더라도 약 1분간은 내부에서 글로벌한 static 변수로인해 Slack File Upload API 호출을 제한시키면서, Redis로 전송 관련 정보 적재만 하는 방식을 사용하였고 이후 Slack API에서 제한이 풀릴 쯤 스케쥴러에 의해 자동으로 전송되도록 했다. 

 

결론

전송 횟수 제한이 있는 Slack API를 사용하면서 이에 대한 우회하는 방안을 생각하고 구현하여 적용해보았다.

해당 방식과 동일하게 적용할 경우 3초 1회보다 빠른 요청이 Redis에 쌓이는 만큼 로컬에 파일이 쌓이는 파일들에 의해 디스크 용량이 부족할 수 있기 때문에 디스크 용량을 넉넉하게 준비해놔야 하는 단점이 있다. 그리고 지연 요청의 경우 많이 쌓인 상태라면, 그 이후 보낸 요청의 경우 늦게 도달하기 때문에 염두할 필요가 있다.

각 자 본인의 서비스에 맞는 구현 방식으로 변경해서 이용해야 할 것이다. 만약 조금 더 서비스 규모가 크거나 파일 전송이 잦아진다면, 별도의 파일서버를 이용하고 WebHook 방식을 사용하여 3초 1회란 제한에서 벗어나, 좀 더 빠르고 안정적으로 대응할 수 있을 것이다.

 

더 나은 방안이 있다면 언제라도 댓글 주세요 ^^

 

Posted by 에스엠에스

댓글을 달아 주세요

목표

 

  스카우터 슬랙 플러그인을 커스터마이징하여 일부 요청에 대한 알림 여부를 다르게 하고, 요청 설정 목록을 서버 중단 없이 갱신하는 기능을 추가한다.

 

* 스카우터에 대한 자세한 설명은 블로그 내 다른 포스트인 https://team-platform.tistory.com/14 에서 다룬다

우리는

 

  개발되어 서비스 중인 자바 프로세스 들을 스카우터를 통해 관리하고 있다. 또한 프로세스들의 장애 알림 처리는 스카우터의 빌트인 플러그인인 슬랙(scouter-plugin-server-slack) 플러그인을 통해 수행하고있다. 이를 통해 스카우터 Xlog에서 에러로 감지된 요청이나 응답이 오래 걸린 요청들은 슬랙을 통해 알림을 받고 있다.

 

근데 그래서 왜?

 

  우리가 운영하는 여러 서비스들의 다양한 요청들 중, 일부의 경우 에러 알림을 굳이 받지 않아야 하는 경우가 있었다. 또한
느린 응답을 가진 요청의 경우에도 슬랙 플러그인 설정 값인 ext_plugin_elapsed_time_threshold 값으로 글로벌하게만 관리되기 때문에, 자체 서비스 요청 응답이 해당 설정 값을 초과할 수밖에 없는 복잡한 로직을 가지거나 수 ms 이내로 빠르게 응답해야만 하는 서비스들의 경우 장애 여부를 판단하기가 곤란했다.

 

예시

Endpoint 로직 수행 시간  글로벌 설정 지연 한계값 슬랙 장애 알림 여부
GET /users:bulk 30,000 ms 5,000 ms O
GET /users/{userID} 10 ms 5,000 ms X

 위 예시에 따르면 첫번째 요청(/users:bulk)의 경우 기본 로직  수행 시간이 길어, 요청할 때마다 슬랙은 느린 응답 장애를 발송할 것이다. 또한 두 번째 요청(/users/{userID})의 경우 1초만 걸려도 장애로 판단되어야 할 경우 글로벌 설정 5초에 의해 느린 응답 알림을 받을 수 없다.


그래서 플러그인을 수정하여 요청별로 각각 지연, 또는 장애에 대한 알림이 발생되더라도 발송 자체를 무시하거나 다른 지연 설정값을 적용받을 수 있도록 하는 시스템 개선이 필요했다.

 

* 슬랙 플러그인 적용에 대한 자세한 사항은 github.com/scouter-contrib/scouter-plugin-server-alert-slack/ 에서 확인하자

 

수정한 부분

 

해당문제를 해결하기 위해선 SlackPlugin.java 파일의 @ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG) 어노테이션이 걸린 메서드를 수정해야 한다.

 

원본 SlackPlugin.java

@ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG)
    public void xlog(XLogPack pack) {

        String objType = AgentManager.getAgent(pack.objHash).objType;

        if (groupConf.getBoolean("ext_plugin_slack_xlog_enabled", objType, true)) {
            if (pack.error != 0) {
                String date = DateUtil.yyyymmdd(pack.endTime);
                String service = TextRD.getString(date, TextTypes.SERVICE, pack.service);
                AlertPack ap = new AlertPack();
                ap.level = AlertLevel.ERROR;
                ap.objHash = pack.objHash;
                ap.title = "xlog Error";
                ap.message = service + " - " + TextRD.getString(date, TextTypes.ERROR, pack.error);
                ap.time = System.currentTimeMillis();
                ap.objType = objType;
                alert(ap);
            }

            try {
                int elapsedThreshold = groupConf.getInt("ext_plugin_elapsed_time_threshold", objType, 0);

                if (elapsedThreshold != 0 && pack.elapsed > elapsedThreshold) {
                    String serviceName = TextRD.getString(DateUtil.yyyymmdd(pack.endTime), TextTypes.SERVICE,
                            pack.service);

                    AlertPack ap = new AlertPack();

                    ap.level = AlertLevel.WARN;
                    ap.objHash = pack.objHash;
                    ap.title = "Elapsed time exceed a threshold.";
                    ap.message = "[" + AgentManager.getAgentName(pack.objHash) + "] " + pack.service + "(" + serviceName
                            + ") " + "elapsed time(" + pack.elapsed + " ms) exceed a threshold.";
                    ap.time = System.currentTimeMillis();
                    ap.objType = objType;

                    alert(ap);
                }

            } catch (Exception e) {
                Logger.printStackTrace(e);
            }
        }
    }

 

해당 메서드는 Xlog의 요청이 기록 됐을 때 진입되는 부분인데 이곳을 보면 에러에 대한 알림을 보내는 영역, 느린 응답에 대한 알림을 보내는 영역이 있는 걸 알 수 있다. 간단하게 슬랙 알림을 무시할 필요가 있는 응답을 추가해보자.

 

수정 SlackPlugin.java

@ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG)
    public void xlog(XLogPack pack) {

        String objType = AgentManager.getAgent(pack.objHash).objType;

        if (groupConf.getBoolean("ext_plugin_slack_xlog_enabled", objType, true)) {
            String serviceName = TextRD.getString(DateUtil.yyyymmdd(pack.endTime), TextTypes.SERVICE, pack.service);
            String agentName = AgentManager.getAgentName(pack.objHash);
            if (pack.error != 0) {
                if ("localhost".equals(agentName)) {
                    // 특정 Agent는 에러 무시
                } else if ("/test".equals(serviceName)) {
                    // /test 요청은 에러 무시
                } else {
                    // 그 외에는 에러 발송
                    String date = DateUtil.yyyymmdd(pack.endTime);
                    String service = TextRD.getString(date, TextTypes.SERVICE, pack.service);
                    AlertPack ap = new AlertPack();
                    ap.level = AlertLevel.ERROR;
                    ap.objHash = pack.objHash;
                    ap.title = "xlog Error";
                    ap.message = service + " - " + TextRD.getString(date, TextTypes.ERROR, pack.error);
                    ap.time = System.currentTimeMillis();
                    ap.objType = objType;
                    alert(ap);
                }
            }

            try {
                int elapsedThreshold = groupConf.getInt("ext_plugin_elapsed_time_threshold", objType, 0);
				boolean slowError = true;
                if (elapsedThreshold != 0 && pack.elapsed < 1000
                        && "/users".equals(serviceName)) {
                        // 1초만 지나도 에러인 응답 체크
                	slowError = false;
                } else if (elapsedThreshold != 0 && pack.elapsed > elapsedThreshold) {
                    if (pack.elapsed < 60000 && "/users:bulk".equals(serviceName)
                    	&& "/gateway-service".equals(agentName)) {
                        // /users:bulk 요청은 60초가 넘어야 에러 발송
                        slowError = false;
                    }
                    ... more
                }
                
                if( slowError ) {
                  AlertPack ap = new AlertPack();

                  ap.level = AlertLevel.WARN;
                  ap.objHash = pack.objHash;
                  ap.title = "Elapsed time exceed a threshold.";
                  ap.message = "[" + agentName + "] " + pack.service + "(" + serviceName + ") " + "elapsed time("
                                  + pack.elapsed + " ms) exceed a threshold.";
                  ap.time = System.currentTimeMillis();
                  ap.objType = objType;

                  alert(ap);
                }

            } catch (Exception e) {
                Logger.printStackTrace(e);
            }
        }
    }

위 처럼 수정 할 경우 /test 요청은 에러가 발생해도 무시되고, /users 는 1초만 지연되더라도, /users:bulk는 60초가 넘어야만 느린 응답 알림이 발생될 것이다.

 

 

 

와 됐다!..

 

됐나..?

 

진짜 문제는..

 

Slack-Plugin은 빌트인 플러그인인 만큼 수정할 경우 스카우터 서버를 재기동해야 하는 단점이 있었다. 또한 하드코딩으로 관리되는 장애 알림 무시 대상 목록의 경우 처음에는 몇 개 되지 않아 관리에 불편함이 없었지만, 지속적으로 개발됨에 따라 점점 개수가 늘어나고 수정 또한 빈번하게 발생되어 관리를 할 필요성이 생겨 다음과 같은 관점에서 변경을 고민해야 했다.

  • 알림 무시 대상의 목록을 수정하면 서버를 재기동하지 않더라도 바로 적용이 되어야 한다

  • 기존 하드코딩으로 구현되었던 조건문을 수용할 수 있도록 간단한 로직이 허용되어야 한다.

  • 알림 무시 대상 목록을 수정할 때 서버 소스의 변경은 최소화해야 한다.

바꾸자

 

 데이터베이스로 알림 무시 대상 목록을 따로 관리하는 방법도 있겠지만, 따로 데이터베이스를 구성해야 하고 jdbc도 직접 구현하거나 다른 라이브러리를 첨부해야 하기에 조금 더 쉬운 방법으로 접근했다. 

먼저 슬랙 플러그인 소스 중 SlackPlugin.java에서 스카우터 관련 설정 파일을 읽어 들이는 방식과 같은 방식으로 파일 기반으로 알림 무시 대상 목록을 관리하고자 했다. scouter 패키지에 속해있는 Configure.java 파일을 보면 3초마다 지정된 경로에 있는 설정 파일을 주기적으로 체크하면서 수정사항이 있을 경우 서버 메모리 Properties 객체에 패치하는 형태로 구성되어있다.

 

이와 같은 파일 갱신 프로세스를 알림 무시 대상 목록에도 적용하기로 했다.

 

SkipConditionalConfigure.java

package scouter.plugin.server.alert.slack.skipconfig;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import scouter.util.FileUtil;
import scouter.util.StringUtil;
import scouter.util.ThreadUtil;

public class SkipConditionalConfigure extends Thread {

    private static SkipConditionalConfigure instance = null;
    public final static String CONDITIONAL_FILE_LOC = "./conditional/";
    private List<SkipConditional> errorSkipConditional = new ArrayList<SkipConditional>();
    private List<SkipConditional> slowSkipConditional = new ArrayList<SkipConditional>();

    public List<SkipConditional> getErrorSkipConditional() {
        return errorSkipConditional;
    }

    public List<SkipConditional> getSlowSkipConditional() {
        return slowSkipConditional;
    }

    public final static synchronized SkipConditionalConfigure getInstance() {
        if (instance == null) {
            instance = new SkipConditionalConfigure();
            instance.setDaemon(true);
            instance.setName(ThreadUtil.getName(instance));
            instance.start();
        }
        return instance;
    }

    private SkipConditionalConfigure() {
        reload(false);
    }

    private boolean isModify = false;

    public boolean isModified() {
        return this.isModify;
    }

    private long last_load_time = -1;
    public Properties property = new Properties();

    private boolean running = true;

    public void run() {
        while (running) {
            reload(false);
            ThreadUtil.sleep(3000);
        }
    }

    private File conditionalFile;

    public File getConditionalFile() throws Exception {
        if (conditionalFile != null) {
            return conditionalFile;
        }
        String s = System.getProperty("scouter.conditional", CONDITIONAL_FILE_LOC + "conditional.conf");
        conditionalFile = new File(s.trim());
        return conditionalFile;
    }

    long last_check = 0;

    public synchronized boolean reload(boolean force) {
        long now = System.currentTimeMillis();
        if (force == false && now < last_check + 3000) {
            return false;
        }
        last_check = now;

        File file = null;
        try {
            file = getConditionalFile();
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        isModify = !(file.lastModified() == last_load_time);
        if (!isModify) {
            return false;
        } else {
            System.out.println("File Modified");
        }

        last_load_time = file.lastModified();

        Properties temp = new Properties();
        if (file.canRead()) {
            FileInputStream in = null;
            try {
                in = new FileInputStream(file);
                temp.load(in);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                FileUtil.close(in);
            }
        }
        property = replaceSysProp(temp);

        setErrorSkipConditionals();
        setSlowSkipConditionals();
        return true;
    }

    public Properties replaceSysProp(Properties temp) {
        Properties p = new Properties();

        Map<Object, Object> args = new HashMap<Object, Object>();
        args.putAll(System.getenv());
        args.putAll(System.getProperties());

        p.putAll(args);

        Iterator<Object> itr = temp.keySet().iterator();
        while (itr.hasNext()) {
            String key = (String) itr.next();
            String value = (String) temp.get(key);
            p.put(key, new scouter.util.ParamText(StringUtil.trim(value)).getText(args));
        }

        return p;
    }

    private void setErrorSkipConditionals() {
        errorSkipConditional.clear();
        int lineNo = 1;
        while (true) {
            try {
                String originStr = getValue("error_alert_skip_conditional_" + lineNo);
                if (originStr == null || "".equals(originStr)) {
                    break;
                }
                SkipConditional conditional = parseSkipConditional(originStr);
                if (conditional == null) {
                    System.out.println("fail skip conditional parse.");
                    break;
                }
                errorSkipConditional.add(conditional);
                lineNo++;
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }
    private SkipConditional parseSkipConditional(String originStr) {
        String[] p1 = originStr.split("\\::");
        if (p1.length == 0) {
            System.out.println("[p1] split length error");
            return null;
        }
        SkipConditional conditional = new SkipConditional();
        String p2;
        if (p1.length > 1) {
            conditional.setElapsedTime(Long.parseLong(p1[0]));
            p2 = p1[1];
        } else {
            conditional.setElapsedTime(0L);
            p2 = p1[0];
        }
        String[] p3 = { p2 };
        if (p2.contains("&&")) {
            p3 = p2.split("\\&\\&");
        } 

        if (p3 == null || p3.length == 0) {
            System.out.println("[p3] split length error");
            return null;
        }
        String[] p4;
        String value = null;
        TargetType targetType = null;
        CompareType compareType = null;

        List<ValueConditional> valueConditionals = new ArrayList<ValueConditional>();
        for (int i = 0; i < p3.length; i++) {
            String vc = StringUtil.trim(p3[i]);
            p4 = vc.split("\\,");
            value = p4[0];
            targetType = TargetType.valueOf(p4[1]);
            compareType = CompareType.valueOf(p4[2]);
            valueConditionals.add(new ValueConditional(value, targetType, compareType));
        }
        conditional.setValueConditionals(valueConditionals);

        return conditional;
    }

    private void setSlowSkipConditionals() {
        slowSkipConditional.clear();
        int no = 1;
        while (true) {
            try {
                String originStr = getValue("slow_alert_skip_conditional_" + no);
                if (originStr == null || "".equals(originStr)) {
                    break;
                }
                SkipConditional conditional = parseSkipConditional(originStr);
                if (conditional == null) {
                    System.out.println("fail skip conditional parse.");
                    break;
                }
                slowSkipConditional.add(conditional);
                no++;
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }

    public List<String> getValueList(String key) {
        String str = StringUtil.trim(property.getProperty(key));
        if (str == null) {
            return Collections.emptyList();
        } else {
            return Arrays.asList(str.split("\\,"));
        }
    }

    public String getValue(String key) {
        return StringUtil.trim(property.getProperty(key));
    }

    public String getValue(String key, String def) {
        return StringUtil.trim(property.getProperty(key, def));
    }
}

 구성은 Configure.class파일과 거의 유사하고, 파일을 갱신하는 핵심 부분의 로직만 옮겨왔다. 추가로 두 개의 알림을 관리하는 목록 클래스로서 errorSkipConditional, slowSkipConditional를 생성, 갱신하는 로직이 추가되어있다.

굳이 로직의 동작을 설명하자면 매 3초마다 지정된 경로(/conditional)의 지정된 파일(conditional.conf)을 읽어 들여, 해당 파일의 수정 시간과 저장된 수정 시간을 비교하여 변경됐을 경우 기존 목록 클래스를 초기화하고 다시 읽어 들인 파일로부터 파싱 과정을 거쳐 목록 클래스를 갱신하는 형태로 구성되어있다.

 

 읽어 들이는 설정 파일의 경우 기존 하드코딩된 로직을 수용할 수 있도록 조건들을 포함해서 구성했다.

UrlPattern 방식을 사용하거나, 일치(equals) 체크 -> 포함(contains) 체크 순으로 검증하는 방식도 생각해 봤지만 기존 하드코딩되어있던 조건의 경우 일치하거나 포함하는 조건으로만 되어있었기 때문에 우선적으로 기존 코드와 맞게 일치, 포함 여부를 명세하는 조건 방식으로 구성하였다.

 

/conditional/conditional.conf

# 에러 알림 무시 목록
# {value},{AGENT|SERVICE},{MATCH|CONTAINS} {&& (optionals)} {other conditional (optionals)}
error_alert_skip_conditional_1=/test,AGENT,MATCH
error_alert_skip_conditional_2=/actuator,AGENT,MATCH
error_alert_skip_conditional_3=/error,AGENT,MATCH

# 느린 응답 알림 무시 목록
# {skipElapsedTime}::{value},{AGENT|SERVICE},{MATCH|CONTAINS} {&&(optionals)} {other conditional (optionals)}
slow_alert_skip_conditional_1=100::/board/1,SERVICE,CONTAINS && /gateway-service,AGENT,MATCH
slow_alert_skip_conditional_2=10000::/users:all,SERVICE,MATCH
slow_alert_skip_conditional_3=30000::/users:bulk,SERVICE,MATCH

  설정 파일의 경우 일반적인 오류와, 느린 응답에 대한 값들로 구성되어있다. 기본적으로 Xlog에 기록되는 객체 값에서 Service, Agent 텍스트 값을 비교하여 알림 여부를 결정하게 된다. 해당 구성 파일로 가능한 것은 특정 지연 시간 설정 이내의, SERVICE 또는 AGENT의 값이 일치(MATCH)하거나, 값을 포함(CONTAINS)할 경우 알림을 무시한다. 고 이해하면 된다.

  기존 Configure.class를 그대로 모방하다 보니, Properties의 구성 형태처럼 Key / Value 형태로 관리해야만 해서 설정 파일의 목록 구성이 순차적으로 이루어져야 하는 아쉬운 점이 있었다. 또한 PathVariable 형태의 값의 경우 RegexPattern기반으로 검사해야 하는 불편함에 제외하고 값이 포함되는 것으로 체크하도록 하였다. 포함으로 체크할 경우 다른 문제가 있는데, 이는 검사 순서를 변경하거나 Pattern 기반으로 변경하긴 해야 한다.

 

설정된 파일이 주기적으로 SkipConditional라고 만들어놓은 객체의 리스트로 갱신되고, 기존 If, else로 처리했던 SlackPlugin.java 에서 다음과 같이 비교하도록 처리된다.

 

SkipConditional.java, ValueConditaional.java, Enum

public class SkipConditional {
    private List<ValueConditional> valueConditionals;
    private long elapsedTime;

    public List<ValueConditional> getValueConditionals() {
        return valueConditionals;
    }

    public void setValueConditionals(List<ValueConditional> valueConditionals) {
        this.valueConditionals = valueConditionals;
    }

    public long getElapsedTime() {
        return elapsedTime;
    }

    public void setElapsedTime(long elapsedTime) {
        this.elapsedTime = elapsedTime;
    }

}

public class ValueConditional {
    private String value;
    private TargetType targetType;
    private CompareType compareType;

    public ValueConditional(String value, TargetType targetType, CompareType compareType) {
        this.value = value;
        this.targetType = targetType;
        this.compareType = compareType;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public TargetType getTargetType() {
        return targetType;
    }

    public void setTargetType(TargetType targetType) {
        this.targetType = targetType;
    }

    public CompareType getCompareType() {
        return compareType;
    }

    public void setCompareType(CompareType compareType) {
        this.compareType = compareType;
    }

}

public enum TargetType {
    SERVICE, AGENT
}

public enum CompareType {
    MATCH, CONTAINS
}
한 개의 SkipConditional.javaconditaional.conf 파일의 한 row와 일치하도록 구성되어있다.

 

SlackPlugin.java의 검사 로직 일부

  // 최상단에Configure와 같은 방식으로 초기화 해준다.
    final SkipConditionalConfigure skipConf = SkipConditionalConfigure.getInstance();
    
    // 알림 무시 검사 로직
    private boolean skipCheck(String agentName, String serviceName, int elapsed) {
        for (SkipConditional con : skipConf.getSlowSkipConditional()) {
            boolean condition = valueConditionalCheck(con, agentName, serviceName);
            long conpareElapsed = con.getElapsedTime();

            if (conpareElapsed > 0) {
                condition = condition && elapsed < conpareElapsed;
            }
            if (condition)
                return condition;
        }
        return false;
    }

    private boolean valueConditionalCheck(SkipConditional con, String agentName, String serviceName) {
        Boolean isSkip = null;
        for (ValueConditional vc : con.getValueConditionals()) {
            String value = null;
            String compareValue = vc.getValue();
            switch (vc.getTargetType()) {
            case AGENT:
                value = agentName;
                break;
            case SERVICE:
                value = serviceName;
                break;
            default:
                return false;
            }
            switch (vc.getCompareType()) {
            case MATCH:
                if (isSkip == null) {
                    isSkip = value.equals(compareValue);
                } else {
                    isSkip = isSkip && value.equals(compareValue);
                }
                break;
            case CONTAINS:
                if (isSkip == null) {
                    isSkip = value.contains(compareValue);
                } else {
                    isSkip = isSkip && value.contains(compareValue);
                }
                break;
            }
        }
        return isSkip;
    }
알림 무시 조건을 분석해서 알림 무시 여부를 리턴해주는 간단한 로직이다.

설정 파일의 구성이나 비교 로직은 입맛에 맞게 상황에 맞게 변경해서 쓰면 좋을 것 같다.

 


스카우터 플러그인 수정 작업을 처음 맡아 어려움이 예상됐지만 보면 바로 알 정도로 플러그인이 간편하게 구성되어 있어 수정에도 큰 어려움이 없었다. 현재는 파일 기반 설정 방식으로 조건이 포함되어 설정 파일 내용이 약간은 난해한 구성인데, 추후 SQLite 같은 small database 형태로 변경해서 다양한 조건들을 직관적으로 구성하여 쉽게 처리할 수 있게 하는 것도 좋을 것 같은 생각이 들었다.

Posted by 에스엠에스

댓글을 달아 주세요

  1. 스카우 2020.10.08 14:35  댓글주소  수정/삭제  댓글쓰기

    정말 좋은 글 잘보았습니다.^^
    해당 기능을 slack이 아닌 로깅파일로 남기려면 어떻게 해야할까요?
    개발자가 아니다보니 말씀하신 내용 이클립스에서 에러없이 컴파일한것만 해도
    벅찬 상태라서요 ㅠ ( 사실 맞게한지도 잘 모르겠습니다 )

    • 에스엠에스 2020.11.20 19:01 신고  댓글주소  수정/삭제

      스카우터 Built-in Server plugin을 직접 작성해서 해결 가능할 걸로 보입니다. 관련한 플러그인 작성 예시 블로그 링크를 공유해 드릴께요. https://m.blog.naver.com/PostView.nhn?blogId=occidere&logNo=221071278283&proxyReferer=https:%2F%2Fwww.google.com%2F

Google AMP 개요 편

Article 2019. 5. 30. 11:55

Google AMP란?

 

AMP는 Accelerated Mobile Pages의 약자로 가속화된 모바일 페이지라고 직역 할 수 있다. Google에서 공개한 오픈소스 라이브러리로 특징으로는 정적 콘텐츠의 빠른 렌더링이 가능한 웹페이지를 제작 할 수 있도록 규격화된 기능을 지원한다. 기존의 웹 페이지 기술을 그대로 사용하고 있고 다양한 브라우저에서 지원되고 있다.

 

 

AMP를 왜 사용하는 것일까?

 

  • 웹 사이트의 성능최적화와 CDN을 무료로 사용 할 수 있다.
  • AMP가 적용된 웹 사이트는 구글 검색 순위에서 우선적으로 노출 될 수 있다.
  • 웹 사이트 제작 도구와 다양한 템플릿을 무료로 제공하고 있다.

 

 

AMP의 주요 기능

 

Google AMP는 웹 페이지를 빠른속도로 렌더링 하기위해 html코드를 작성하기 위한 규격이 있다. AMP HTML과 AMP JS라이브러리 Google AMP 캐시를 사용한다.

 

  • AMP HTML : AMP전용 HTML로 페이지 내의 태그의 형태는 보통의 HTML 코드와 같으나 일부 HTML 태그들은 AMP 전용태그가 사용되고 있다.
  • AMP JS : AMP JS 라이브러리는 모든 AMP 성능 권장사항을 구현하였고 리소스 로딩을 관리하며 맞춤 태그를 지원하고 있어 빠른 페이지 렌더링을 보장한다.
  • Google AMP 캐시 : AMP HTML페이지를 가져와 캐시하여 자동으로 페이지 성능을 개선한다. 문서와 모든 JS파일 및 이미지가 하나의 출처에서 로드되므로 효율성이 극대화 된다.

 

 

AMP의 작동 원리

 

AMP 페이지의 렌더링이 빠르게 이루어지는 이유는 AMP가 가지고 있는 일련의 규격에 맞추어 작동하고 있기 때문이다.

 

  • 비동기 스크립트만 허용 : 코드에 따라 DOM 구성을 차단하고 페이지 렌더링을 지연시키는 부작용이 있기때문에 규격화 된 커스텀 AMP 요소를 통해 구현해야 한다.
  • 모든 리소스의 사이즈를 정적으로 지정 : 외부 리소스 다운로드 전 HTML 요소들의 사이즈를 지정해주면 리소스 다운로드 여부와 관계없이 레이아웃먼저 로드할 수 있다.
  • 외부 리소스에 의한 렌더링 차단 방지 : AMP는 기본적으로 유튜브, 트위터, 인스타그램같은 외부 리소스의 로드를 위해 커스텀 태그를 지원하고 있고 커스텀 태그 사용 전 아래와 같이 스크립트를 로드 해주어야 한다.
<script async custom-element="amp-iframe" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>
  • 기본 페이지에서 모든 외부 자바스크립트 제외 : 외부에서 로드되는 자바스크립트는 동기식 로딩이 많아 페이지 로드에 지연을 발생 시킬 수 있다. 때문에 기본 페이지 내의 iframe에서만 외부 자바스크립트를 허용한다.
  • 모든 CSS는 Inline 방식이며 크기가 한정됨 : CSS는 모든 렌더링과 페이지 로드를 차단하고 용량이 과도하게 커지는 경향이 있다. 때문에 AMP에서는 CSS의 작성을 인라인 스타일만 허용하고 있고 용량 또한 50KB 이하로 제한하고 있다.
  • 효율적인 폰트 트리거 : 웹 폰트는 용량이 매우 크기때문에 성능을 위해서는 웹 폰트 최적화가 필수이다. AMP에서는 페이지 로드 시 폰트부터 다운로드 후 자바스크립트와 인라인 스타일 시트가 로드된다.
  • 스타일 재계산 최소화 : 요소의 크기를 지정할 때마다 스타일 재계산이 트리거 되는데 브라우저 페이지에서 전체 페이지를 다시 레이아웃해야 하기때문에 페이지 로드 속도가 느려진다. AMP 페이지에서는 DOM 읽기가 모두 끝난 후에 스타일이 재계산 되므로 프레임마다 최대 한 번만 재계산 되어 성능 저하를 방지 할 수 있다.
  • GPU 가속 애니메이션만 실행 : 성능 향상을 위해 레이어 애니메이션은 GPU에서 처리하도록 하고 페이지 레이아웃 업데이트 구간에서는 성능 저하가 발생할 수 있으므로 애니메이션에 관련하여 CSS에 대한 규칙을 지정하고 GPU 가속이 적용되는 애니메이션만 사용하도록 한다.
  • 우선순위별 리소스 로드 : AMP는 모든 리소스에 대한 다운로드를 제어하며 리소스 로드에 우선순위를 지정하여 필요한 리소스는 로딩하고 바로 로딩할 필요가 없는 리소스는 데이터를 미리 가져와서(prefetches) 로드 대기 한다.
  • 즉각적인 페이지 로드 : AMP는 대역폭과 CPU 사용량을 줄이도록 최적화 되어 있으며 사용자가 명시적으로 이동 의사를 밝히기 전에 목표 페이지를 렌더링해 두었다가 실제 페이지를 선택 할 때 즉시 로드 할 수 있다.

 

 

AMP 페이지의 기본구조

<!doctype html>
<html ⚡ lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">

    <link rel="canonical" href="/article.html">
    <link rel="shortcut icon" href="amp_favicon.png">

    <title>News Article</title>

    <style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
    <style amp-custom>
      body {
        width: auto;
        margin: 0;
        padding: 0;
      }

      header {
        background: Tomato;
        color: white;
        font-size: 2em;
        text-align: center;
      }

      h1 {
        margin: 0;
        padding: 0.5em;
        background: white;
        box-shadow: 0px 3px 5px grey;
      }

      p {
        padding: 0.5em;
        margin: 0.5em;
      }
    </style>
    <script async src="https://cdn.ampproject.org/v0.js"></script>
    <script type="application/ld+json">
    {
     "@context": "http://schema.org",
     "@type": "NewsArticle",
     "mainEntityOfPage":{
       "@type":"WebPage",
       "@id":"https://example.com/my-article.html"
     },
     "headline": "My First AMP Article",
     "image": {
       "@type": "ImageObject",
       "url": "https://example.com/article_thumbnail1.jpg",
       "height": 800,
       "width": 800
     },
     "datePublished": "2015-02-05T08:00:00+08:00",
     "dateModified": "2015-02-05T09:20:00+08:00",
     "author": {
       "@type": "Person",
       "name": "John Doe"
     },
     "publisher": {
       "@type": "Organization",
       "name": "⚡ AMP Times",
       "logo": {
         "@type": "ImageObject",
         "url": "https://example.com/amptimes_logo.jpg",
         "width": 600,
         "height": 60
       }
     },
     "description": "My first experience in an AMPlified world"
    }
    </script>
  </head>
  <body>
    <header>
      News Site
    </header>
    <article>
      <h1>Article Name</h1>

      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam egestas tortor sapien, non tristique ligula accumsan eu.</p>

      <amp-img src="mountains.jpg" layout="responsive" width="266" height="150"></amp-img>
    </article>
  </body>
</html>

 

 

AMP 페이지 필수 요소 설명

 

  • AMP 페이지임을 명시
    • 최상위 태그로 <html ⚡ > 또는 <html amp>을 선언하여 AMP 문서로 인식 될 수 있도록 한다.
<html ⚡> 
<!-- 또는 -->
<html amp>
  • AMP JS 라이브러리 로드
<script async src="https://cdn.ampproject.org/v0.js"></script>
  • AMP 보일러플레이트 코드 추가
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>

 

 

AMP 이미지 태그 설정

<amp-img src="mountains.jpg" layout="responsive" width="266" height="150"></amp-img>
  • AMP에서는 이미지를 추가 하기 위해서는 전용 태그를 사용해야 한다.
  • 이미지를 추가 할 때 사이즈(width, height)는 반드시 지정해야 한다.
  • 이미지 표기를 위해 frame, object, param, embed 등 일부 html태그는 AMP에서 사용 할 수 없다.

 

 

AMP 커스텀 CSS 설정

<style amp-custom>
      body {
        width: auto;
        margin: 0;
        padding: 0;
      }

      header {
        background: Tomato;
        color: white;
        font-size: 2em;
        text-align: center;
      }
</style>
  • AMP 페이지에서 사용하기 위한 커스텀 CSS를 설정한다.
  • 커스텀 CSS는 <head> 태그 내부에 인라인으로 설정 되어야 한다.
  • 커스텀 CSS는 최대 50KB를 넘을 수 없다.
  • !important를 사용할 수 없으며 외부 스타일 참조가 불가능하다.

 

 

AMP Link 페이지 연결

 

  • AMP가 적용된 페이지와 적용되어있지 않은 페이지를 둘 다 가진 사이트가 존재하고 Google 검색에서 AMP가 적용되어있지 않은 페이지를 찾았을 때 AMP가 적용된 페이지로 연결 할 수 있는 기능이다.
  • AMP가 적용되어있지 않은 페이지에 amp페이지를 링크하고 amp 페이지에는 non-amp페이지를 링크한다.
<!-- 아래 코드를 NON-AMP 페이지에 추가 -->
<link rel="amphtml" href="/article.amp.html">
<!-- 아래 코드를 AMP 페이지에 추가 -->
<link rel="canonical" href="/article.html">
  • AMP 페이지 하나만 존재하더라도 canonical 링크를 반드시 페이지에 시켜주어야 한다.
<link rel="canonical" href="/article.html">

 

 

AMP 샘플 페이지 구동

 

  • 상단에 게재한 샘플 페이지 코드 실행 결과

 

 

결론

 

  • 장점
    • 웹 페이지의 최적화를 통해 성능 향상을 기대 할 수 있다.
    • 구글 검색에서 상단에 노출될 확률이 높다.
    • 다양한 템플릿 제공으로 빠르게 웹 사이트를 만들어낼 수 있다. (흡사 부트 스트랩 같은 느낌)
    • 구글 애널리틱스 연동을 amp-analytics 스니펫으로 제공하고 있고 수집 정보를 유연하게 설정 할 수 있다.

  • 단점
    • 거의 모든 태그가 규격화 되어있고 일부는 AMP전용 태그를 사용해야 한다.
    • AMP 전용 JS 라이브러리를 사용하고 커스텀하게 자바스크립트를 사용 할 수 없다.
    • 커스텀 CSS는 인라인으로만 사용해야 하며 50KB를 넘지 않아야 하고 !important같은 일부 요소는 사용이 금지 되어있다.
    • 복잡한 자바스크립트 코드 또는 역동적인 애니메이션 효과가 포함된 페이지는 AMP화 시키기 어렵다.

 

 

Google AMP 개요 편

끝.

 

Posted by DevStream

댓글을 달아 주세요

지난 도커파일 편에서 원하는 기능이 담긴 이미지를 손쉽게 만들 수 있는 도커파일의 작성방법과 이미지 생성, 컨테이너 생성 실습을 진행 했었다. 이번 Docker Compose(이하 도커 컴포즈) 편 에서는 도커 컴포즈의 개요와 YAML파일의 작성 및 실습을 진행 해보도록 하겠다.

 

 

Docker Compose 개요

 

웹서버 기능을 하는 어플리케이션이 있다고 가정 해보자 여기에는 DB와 Web Server가 필요하고 이것을 각각의 컨테이너로 나누었다. 제대로 동작 하는지 확인 해보기 위해서는 각 컨테이너들을 실행 시켜야 하는데 docker run 명령어를 이용하여 컨테이너를 생성하려면 두 번의 run 명령어 입력이 필요하다.

컨테이너를 가끔 생성하는 경우라면 괜찮겠지만 컨테이너의 생성빈도가 높고 실행 시켜야하는 컨테이너가 지금보다 더 늘어난다면 상당히 번거로운 작업이 된다. 이런 경우 여러개의 컨테이너를 한번에 실행 시키고 관리 할 수 있다면 효율적일 것이다. 도커 컴포즈는 위와 같이 여러 컨테이너를 한번에 관리 할 때 아주 유용하다. YAML(확장자 *.yml) 파일을 이용하여 어떠한 이미지를 사용하여 어떤 컨테이너를 어떻게 실행 시킬 것인지 기술해주면 도커는 해당 내용대로 컨테이너를 순차적으로 실행 시킨다.

 

 

Docker Compose YAML 파일 작성

 

먼저 한 어플리케이션의 구성요소인 MariaDB, Apache를 일반적으로 컨테이너화 할 경우 보통은 아래와 같이 run 명령어를 사용하여 이미지를 내려받고 컨테이너를 실행 시킬 것이다.

[root@localhost testuser]# docker run \
--name mdb \
-e MYSQL_ROOT_PASSWORD='1234' \
-d mariadb:latest
da6943f279767932259d28dfd6d0f92cb3c90499569c3907a6e08594e58b8b54

[root@localhost testuser]# docker run \
--name web \
-it \
-p 80:80 \
--link mdb:mdb docker_img
 * Restarting OpenBSD Secure Shell server sshd
 root@2ae7ab2da8df:/#

 

위 두개의 run 명령어를 YAML 파일로 작성해보자. (※주의 : 탭 인식 못하므로 스페이스 두칸으로 구분)

version: '3'
services:
  mdb:
    image: mariadb:latest
    environment:
      MYSQL_ROOT_PASSWORD: 1234
  web:
    build: ./dockerfile
    image: apache_df:web
    ports:
      - "80:80"
    links:
      - mdb:mdb

version : YAML파일 포멧의 버전을 의미하고 여기서는 3버전을 사용하였다.

services : 생성될 컨테이너의 정보를 담고 있다. services 바로 아래에 서비스명을 기술 할 수 있으며 위에 기술 된 mdb, web이 서비스명이 된다.

image : docker images 명령어를 실행하면 노출되는 repository명이다.

environment : docker run 명령어 옵션 -e와 같으며 위에 기술한 MYSQL_ROOT_PASSWORD와 같이 컨테이너 생성 시 들어갈 환경 변수를 지정 해줄 수 있다.

build : 지정된 경로 내에 존재하는 도커파일을 실행하여 이미지로 만들고 그 이미지로 컨테이너를 생성한다.

ports : docker run 명령어 옵션 -p와 같으며 해당 컨테이너 내에서 오픈 할 포트번호를 지정 할 수 있다.

links : docker run 명령어 옵션 --link와 같으며 연결할 서비스명을 입력하여 해당 서비스로 접근 할 수 있다.

 

 

Docker Compose 실행

YAML 파일 작성이 완료 되었으면 도커 컴포즈 파일을 docker-compose.yml이라는 이름으로 저장해준다. 필자는 /home/testuser/docom 경로에 저장하였다. 도커 컴포즈 실행 전 실습을 위해 이전에 만들었던 컨테이너들을 모두 지워주도록 하자.

# docker rm -f $(docker ps -a -q)
2ae7ab2da8df
da6943f27976
# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

 

이어서 도커 컴포즈를 다운로드 받는다.

# sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   617    0   617    0     0     98      0 --:--:--  0:00:06 --:--:--   163
100 15.4M  100 15.4M    0     0   754k      0  0:00:20  0:00:20 --:--:-- 1840k
# sudo chmod +x /usr/local/bin/docker-compose
# ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
# docker-compose --version
docker-compose version 1.24.0, build 0aa59064

 

다운로드가 완료 되었으면 위에 작성한 YAML파일을 기반으로 도커 컴포즈를 실행 시켜보자.

Apache2는 미리 기술해둔 dockerfile을 통해 image를 생성하였다. 그리고 해당 이미지를 통해 docom_web_1 이라는 컨테이너를 생성한 것을 확인하였다.

# pwd
/home/testuser/docom
# docker-compose up -d
Creating network "docom_default" with the default driver
Building web
Step 1/6 : FROM ubuntu:14.04
 ---> 5dbc3f318ea5
Step 2/6 : MAINTAINER pamtrak06 <pamtrak06@gmail.com>
 ---> Using cache
 ---> 9c561b8834f8
Step 3/6 : RUN apt-get update && apt-get install -y apache2 apache2-threaded-dev
 ---> Using cache
 ---> 09fa1cec80dc
Step 4/6 : RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
 ---> Using cache
 ---> ff1eee1b0bdb
Step 5/6 : CMD apachectl -D FOREGROUND
 ---> Using cache
 ---> e8563809e2c6
Step 6/6 : EXPOSE 80
 ---> Using cache
 ---> d307433464d8
Successfully built d307433464d8
WARNING: Image for service web was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating docom_mdb_1 ... done
Creating docom_web_1 ... done

 

docker ps -a 명령을 통해 컨테이너가 제대로 생성 되었는지 다시 한번 확인 해보고 이번 편을 마치도록 하겠다.

[root@localhost docom]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
044eba02fb3c        apache_df:web       "apachectl -D FORE..."   11 seconds ago      Up 10 seconds       0.0.0.0:80->80/tcp   docom_web_1
ede97239d9bc        mariadb:latest      "docker-entrypoint..."   11 seconds ago      Up 11 seconds       3306/tcp             docom_mdb_1

컨테이너가 잘 생성되었다.

 

 

이번 Docker : Docker Compose 편 에서는 여러개의 컨테이너를 도커 컴포즈를 이용하여 하나로 묶는 개념으로 다소 복잡할 수 있는 분산된 컨테이너를 효과적으로 관리할 수 있도록 YAML파일을 기술하고 실습해보았다. 특히 Dockerfile을 같이 활용하여 사용 할 경우 언제 어느 환경에서도 즉시 컨테이너를 생성하고 관리 할 수 있게 되었다.

 

 

Docker : Docker Compose 편

끝.

'Article' 카테고리의 다른 글

Scouter Slack Plugin 알림 설정하기  (2) 2020.06.03
Google AMP 개요 편  (0) 2019.05.30
Docker : Docker Compose 편  (0) 2019.04.05
자연어처리 - 데이터 정제  (0) 2019.03.22
REST API 디자인 가이드 적용기  (0) 2019.03.08
자연어처리 - Bag of words, n-gram  (0) 2019.03.08
Posted by DevStream

댓글을 달아 주세요