'전체 글'에 해당하는 글 2273건

일반적으로 EKS cluster 를 생성한 계정 이외에 다른 IAM 사용자나 role 을 세분화하여 추가할 수 있으며, 심플한 사용자 추가를 진행해 보았다. 예전에 뭣 때문에 eksctl 을 설치했는지는 모르겠다만 땡큐~

 

eksctl 이 설치되어 있지 않다면... https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/eksctl.html

 

eksctl 설치 또는 업데이트 - Amazon EKS

GitTag 버전은 0.105.0 이상이어야 합니다. 그렇지 않은 경우 터미널 출력에서 설치 또는 업그레이드 오류가 있는지 확인하거나, 1단계의 주소를 https://github.com/weaveworks/eksctl/releases/download/v0.105.0/eksct

docs.aws.amazon.com

 

eksctl 을 이용하면 간단히 cluster 정보와 추가할 iam 계정의 arn, ClusterRoleBinding group 을 지정한다.

 

$ eksctl create iamidentitymapping \
    --cluster cluster-name \
    --region=ap-northeast-2 \
    --arn arn:aws:iam::111122223333:user/ggamzzak \
    --group system:masters \
    --profile my-profile

2022-08-02 17:25:32 [ℹ]  eksctl version 0.75.0
2022-08-02 17:25:32 [ℹ]  using region ap-northeast-2
2022-08-02 17:25:32 [ℹ]  adding identity "arn:aws:iam::111122223333:user/ggamzzak" to auth ConfigMap

 

"system:masters" 그룹의 자격 증명을 사용하면 cluster-admin 슈퍼유저의 역할을 바인딩하여 모든 작업을 수행할 수 있다.

 


 

configmap/aws-auth 를 확인하면 다음과 같이 추가되어 있다.

 

$ kubectl describe configmap -n kube-system aws-auth
Name:         aws-auth
Namespace:    kube-system
Labels:       <none>
Annotations:  <none>

Data
====
mapUsers:
----
- groups:
  - system:masters
  userarn: arn:aws:iam::111122223333:user/ggamzzak 

mapRoles:
...

 

또는,

 

$ eksctl get iamidentitymapping --cluster my-cluster --region=ap-northeast-2 --profile my-profile
2022-08-02 20:52:23 [ℹ]  eksctl version 0.75.0
2022-08-02 20:52:23 [ℹ]  using region ap-northeast-2
ARN                                                             USERNAME                        GROUPS                                                                          ACCOUNT
...
arn:aws:iam::111122223333:user/ggamzzak                                                         system:masters

 

추가된 IAM 계정으로 테스트 ㄱㄱ~

 

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

트랙백  0 , 댓글  0개가 달렸습니다.
secret

kubernetes 를 사용하면서 불편했던 점 중에 하나는 서비스 로깅이었다. 로컬에서 가장 자주 사용하던 명령어가 바로...

 

> kubectl logs -n namespace -f <pod-name>

 

배포(deployment) 하고 나서 로깅/디버깅을 위해 pod 리스트를 나열하고 pod-name 으로 로깅하는 짓이 가장 번거로웠다. 지금은 pod 가 한 10개 정도인데... 창을 10개를 띄워놓을 수도 없고... 그래서 실시간으로 pods 정보를 보여주고, 해당 로그를 스트리밍으로 좀 더 편하게 볼 수 있는 툴을 찾아보기로 했다.

 

쿠버네티스 모니터링 도구를 찾아보니 꽤나 많이 보이는데 그 중에 몇 가지만 테스트해 보았다.

 

  1. Kubernetes Dashboard
    심플하다. 시스템 모니터는 다른 모든 툴들도 기본적으로 잘 동작한다. CPU / RAM / Workload 등... 하지만 서비스 로깅이 불편하다. 5초 reload 방식... 이것도 대부분의 툴들이 마찬가지.

  2. Prometheus / Grafana
    설치 개복잡. Helm 을 사용하면 조금 편함. 잘나가다가 promtail 설정에서 막혔음. Fargate 사용 유저는 엄청난 고뇌가 필요할 듯... 몇가지 대안은 있었지만 로그 하나 보겠다고 굳이 일 벌리고 싶지 않았음. 가장 보편적이기도 하고, Custom / 확장도 좋은 것 같고... 대부분의 회사들이 사용하니 이건 써봐야겠지?

  3. Datadog
    시스템 통합으로 사용하고는 있으나... 쿠버네티스에 특화되어 있다기 보다는 로깅 분석에 어울리는... 그렇다고 안되는 건 없는... 그렇다고 굳이 유료 결제할 필요도 없는...?

  4.  Lens (Desktop) 
    Kubernetes IDE 답게 쿠버네티스에 특화되어 있음. 얼핏보면 외관은 Prometheus 와 흡사. 클릭 몇번으로 설치 완료. Attach Pod / logs 메뉴로 실시간 로깅은 잘됨. 혼자쓰기 괜츈.

 

kubernetes-lens

 

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

트랙백  0 , 댓글  0개가 달렸습니다.
secret

CORS error

Programming/Spring Boot 2022. 3. 10. 21:13

CORS(Cross-Origin Resource Sharing)는 웹 브라우저에서 특정 사이트(A.com) 로부터 다른 사이트(B.com) 로의 요청 및 접근을 가능하게 하는 프로토콜이다. 보안 상 최신 브라우저는 기본적으로 타 사이트로의 요청을 제한하고 있다. SPA 프레임워크의 사용 등으로 frontend 와 backend 의 분리가 일반적이 된 요즘, 서버로의 API 요청시 프로토콜/도메인/포트 등이 다르다면, CORS 설정이 올바르지 않다면 브라우저 콘솔에서 관련 error 를 마주하게 된다.

 

Access to XMLHttpRequest at 'http://localhost:8080/account/signin' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

 

기본적인 CORS 요청들을 단순(simple) 요청이라 할 때, application/json 타입의 요청이거나 커스텀 헤더를 전송하는 등의 요청은 사전(preflight) 요청이라고 한다. 이 경우는 웹 브라우저에서 OPTIONS 메소드로 preflight(사전전달) 요청이 먼저 수행하고, 서버에서 허용 범주를 응답 받은 후에 실제 GET/POST 등의 요청을 보내게 된다.

 

preflight

 

 

CORS 서버 설정

 

서버 개발자는 요청을 허용할 범주(Access-Control-Allow-* header/method/origin) 를 설정하여 응답 헤더에 보낼 수 있도록 세팅하면 된다. Spring Boot 의 경우, WebMvcConfigurer 를 구현하고 addCorsMappings 메소드를 오버라이드 하여 아래와 같이 설정할 수 있다.

 

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:3000", "http://localhost:8081")
                .exposedHeaders("Authorization");
    }
}

 

exposedHeaders 메소드의 경우 브라우저에서 클라이언트가 접근 가능한 응답 헤더 목록을 설정한다. (Authorization 은 token 등을 이용할 때 많이 사용) cors mapping 만 설정했을 때는 GET, HEAD, POST 의 메소드에 대해서, 모든 출처의 요청에 대해 허용하는 것이 기본값이다. 위와 같이 Spring 에서 설정했을 경우, 아래와 같은 응답 헤더를 보내주고 CORS 는 성공하게 된다.

 

Access-Control-Allow-Headers: content-type
Access-Control-Allow-Methods: GET,HEAD,POST
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Expose-Headers: Authorization

 

컨트롤러, 메소드 단위에서 @CrossOrigin 어노테이션을 사용할 수도 있다.

 

 

또한, Spring Security 를 사용중이라면 아래와 유사한 방식으로 cors 요청을 처리할 수 있다.

 

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .and()
            .cors()
            .and()...
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.addAllowedOrigin("http://localhost:3000");
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

트랙백  0 , 댓글  0개가 달렸습니다.
secret

Spring Boot 에서 모든 원본 Http request 와 response 에 대한 log 를 기록하려고 한다.

 

요청 → Filter → Interceptor → AOP → Interceptor → Filter → 응답 의 처리 순서로 볼 때, 가장 원시적인 요청 본문과 응답 본문을 캐치할 수 있는 부분은 Filter 였다. 요청을 받자마자 Filter 에서 본문을 로깅하고, 응답 직전에 본문을 로깅하고... 하지만 이거슨 순수한 나만의 착각?

 

 

spring-mvc-request-lifecycle

 

 

Request / Response body 로깅 목표와... 결과

  • 비정상 요청 파악 및 비정상 응답 파악하기.
  • 최대한 원시 본문을 유지할 것. (의미를 찾지못함;)
  • 요청 본문과 응답 본문의 로깅을 각각 처리. (막상 분리하고 나니 굳이...)
  • POST 메소드만 처리.
  • form-urlencoded 와 application/json 모두 처리.
  • 로깅 위치는 Filter 와 Interceptor 의 preHandle 과 afterCompletion 를 적절히 사용.

 


 

1. request / response 본문(body) 읽기

 

request 와 response 의 본문(body) 를 로깅하기 위해 HttpServletRequest 의 getReader() 나 getInputStream() 등을 통해서 읽어낼 경우, 다음 단계에서 이 본문들은 비어있는 상태가 된다. 인터셉터에서 request 본문을 읽는다면 컨트롤러에 파라미터가 전달되지 않을 것이고, 인터셉터에서 response 본문을 읽는다면 클라이언트에 내용이 빈 상태로 응답될 수 있다. 이를 방지하기 위해 사용하는 방법이 여러가지 있는데 난 그 중에서 ContentCachingRequestWrapper / ContentCachingResponseWrapper 클래스를 선택했다.

 

 

2. ContentCachingRequestWrapper / ContentCachingResponseWrapper

 

ContentCachingRequestWrapper 는 getReader() 나 getInputStream() 등을 통해서 읽은 요청 본문을 캐시하고 byte array 로 이를 읽을 수 있게 하는 HttpServletRequest 래퍼이다. ContentCachingResponseWrapper의 경우 Body를 돌려주기 위해 copyBodyToResponse method를 호출해줘야 한다.

 

아래는 filter 파일 하나에 전부 때려넣은 예제이다.

 

@Component
public class LoggingFilter implements Filter {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {

        if (servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            HttpServletResponse response = (HttpServletResponse) servletResponse;

            HttpServletRequest requestToCache = new ContentCachingRequestWrapper(request);
            HttpServletResponse responseToCache = new ContentCachingResponseWrapper(response);

            chain.doFilter(requestToCache, responseToCache);

            logger.info("request header: {}", getHeaders(requestToCache));
            logger.info("request body: {}", getRequestBody((ContentCachingRequestWrapper) requestToCache));
            logger.info("response body: {}", getResponseBody(responseToCache));
           
        } else {
            chain.doFilter(servletRequest, servletResponse);
        }
    }

    private Map<String, Object> getHeaders(HttpServletRequest request) {
        Map<String, Object> headerMap = new HashMap<>();

        Enumeration<String> headerArray = request.getHeaderNames();
        while (headerArray.hasMoreElements()) {
            String headerName = headerArray.nextElement();
            headerMap.put(headerName, request.getHeader(headerName));
        }
        return headerMap;
    }

    private String getRequestBody(ContentCachingRequestWrapper request) {
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                try {
                    return new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException e) {
                    return " - ";
                }
            }
        }
        return " - ";
    }

    private String getResponseBody(final HttpServletResponse response) throws IOException {
        String payload = null;
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            wrapper.setCharacterEncoding("UTF-8");
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
                wrapper.copyBodyToResponse();
            }
        }
        return null == payload ? " - " : payload;
    }
}


/*
2022-03-01 01:37:04.049  INFO 16344 --- [nio-8080-exec-6] com.example.api.filter.LoggingFilter     : request header: {content-length=51, postman-token=d4e2bfa4-1...
2022-03-01 01:37:04.050  INFO 16344 --- [nio-8080-exec-6] com.example.api.filter.LoggingFilter     : request body: {
    "param1": "test1",
    "param2": "test2"
}
2022-03-01 01:37:04.051  INFO 16344 --- [nio-8080-exec-6] com.example.api.filter.LoggingFilter     : response body: {"res1":"test1","res2":"test2","res3":"test3"}
*/

 

Filter 에서 ContentCachingRequestWrapper / ContentCachingResponseWrapper 래퍼로 request 와 response 를 캐시하여, 재사용이 가능하도록 하였다. 밑에 메소드 3개는 각각 헤더, 요청 본문, 응답 본문 가져오기.

 

 

이렇게 ContentCaching* 클래스로 간단하게 요청/응답 본문을 로깅할 수 있었다. 심플한건 좋았지만, 내가 원했던 방법은 이렇게 filter 에 전부 때려박는 것은 아니었다. 요청 본문은 preHandle 에서 로깅하고 싶었는데, 제약 조건이 많았다. preHandle 에서 ContentCachingRequestWrapper 로는 json 을 처리하지 못했다. 하지만 form-urlencoded 요청의 경우, application.properties 에서 logging.level.org.springframework.web=debug 설정이 되어 있다면, 인터셉터 preHandle 에서도 요청 본문 로깅이 가능했다. Exception 발생시 응답 본문은 null 이 발생했으나, 이 경우 상태 코드로 응답 예측이 가능하니 큰 문제는 없었다. multipart/form-data 요청은 용량 차지하지 않게 별도로 알아서 잘 처리하시고... (난 그냥 본문 날려버렸음.)

 

굳이 Spring 에서 처리하긴 했지만, 로깅을 커스텀 하는 것은 쉽지 않았다. 어떤 서비스에 어떤 목적으로 어디에 로깅을 하느냐 또 그 로깅을 어떻게 활용할 것인가가 중요한 것 같다.

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

트랙백  0 , 댓글  1개가 달렸습니다.
  1. 쪼무래기 2022.07.28 16:49
    안녕하세요, 저는 지금 인턴 생활하면서 로그 시스템 구현하고 있었는데요, 여러 포스팅 중에 올려주신 포스팅이 가장 유익했습니다ㅎㅎ 감사합니다.
    질문 하나 드리고 싶은데요. "preHandle 에서 ContentCachingRequestWrapper 로는 json 을 처리하지 못했다." 이 부분은 어떤 경로로 학습해야 알 수 있는거죠??
secret

Spring API 프로젝트에서 공통적인 에러 코드로 응답하기.

 

API 개발시 어떠한 처리 결과에도 동일한 포맷의 응답(response) 을 리턴하는 것은 중요하다. 아래는 가장 간단한 예이다.

 

{
    "code": 200,
    "message": "ok",
    "result": {}
}

 

code / message 로 미리 정의된 응답 코드와 메시지를 확인할 수 있고, 정상적으로 처리가 되었을 경우 result 에서 결과를 확인할 수 있다. 디테일이 필요하다면 더 다양한 코드에 상세한 설명을 넣으면 되고, 보안을 강화한다면 메시지를 없앨 수도 있다. 이러한 공통적인 포맷을 Spring 에서 구현하는 방법을 살펴 보았다.

 

 

1. whitelabel 에러 페이지

 

Spring Boot 는 기본적으로 text/html 타입의 요청에 대해 whitelabel 이라는 오류 페이지를 제공한다. 기존 톰캣의 보라색 에러 페이지 보다는 괜찮아 보일 수 있다.

 

 

whitelabel-spring

 

브라우저에서 접속했을 때에도 에러 페이지를 커스텀 하려면 properties 파일에서 whitelabel 페이지를 비활성화 하고 에러 페이지를 재정의해야 한다.

 

# application.properties
server.error.whitelabel.enabled=false

 

 

2. 기본 json 에러 응답

 

Spring Boot 는 기본적으로 RESTful 이나 application/json 타입의 요청에 대해 basicErrorController 로부터 아래와 같은 포맷으로 응답한다.

 

{
    "timestamp": "2022-02-15T04:28:46.310+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/a"
}

 

위 속성은 DefaultErrorAttributes 에서 정의되는데 속성만 추가하려면 application.properties 에서 server.error 옵션들을 수정하던지 DefaultErrorAttributes 를 상속하여 커스텀 할 수도 있다.

 

# message 항목 보이기
server.error.include-message=always

 

@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> result = super.getErrorAttributes(webRequest, options);
        result.put("description", "abcdefg");
        return result;
    }
}

 

 

3. AbstractErrorController 상속하여 재정의

 

basicErrorController 처럼 ErrorController 인터페이스를 구현한 AbstractErrorController 를 상속하여 에러 응답을 재정의할 수 있다.

 

@RestController
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomErrorController extends AbstractErrorController {

    public CustomErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }

    @RequestMapping
    public ResponseEntity<ErrorMessageEntity> customError(HttpServletRequest request) {
        HttpStatus httpStatus = getStatus(request);

        CustomResponseEntity customResponseEntity = new CustomResponseEntity();
        customResponseEntity.setCode(httpStatus.value());
        customResponseEntity.setMessgae(httpStatus.getReasonPhrase());
        customResponseEntity.setResult(null);

        return new ResponseEntity<>(customResponseEntity, httpStatus);
    }
}

 

@Entity
public class CustomResponseEntity {
    private Integer code;
    private String messgae;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String result;
    ...
}

 

기본 에러 경로인 /error (properties 파일: server.error.path) 는 basicErrorController 에서 매핑을 시도하므로, ErrorController 인터페이스를 구현하지 않고 /error 컨트롤러를 사용한다면 런타임 에러가 발생한다.

 

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'basicErrorController' method

 

 

4. ResponseStatusException 사용

 

위와 같이 AbstractErrorController 를 상속하면 모든 HTTP 상태 코드에 대한 응답을 커스텀 할 수 있다. 여기에 Spring 5+ 를 사용 중이라면 RuntimeException 을 상속하는 ResponseStatusException 를 이용해 어디에서든 원하는 에러를 발생시킬 수 있다.

 

throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path");

 

특정 상황에 특정 에러 코드와 메시지로 응답하고 싶었는데 2번째 파라미터를 응답 엔티티에 넣는 것이 쉽지 않았다.

 

 

5. Custom Exception 생성

 

ResponseStatusException 을 대신하여 메시지도 응답할 수 있도록 CustomException 을 생성한다.

 

@Component
public class CustomException extends RuntimeException {

    private ErrorCode errorCode;

    public CustomException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return this.errorCode;
    }
}

 

에러 메시지를 enum 으로 정리한다.

 

public enum ErrorCode {
    
    EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Expired Token"),
    INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid Token"),
    SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"),
    ...
    ;

    private final HttpStatus status;
    private final String message;

    ErrorCode(HttpStatus status, String message) {
        this.status = status;
        this.message = message;
    }

    public HttpStatus getStatus() {
        return this.status;
    }

    public String getMessage() {
        return this.message;
    }
}

 

컨트롤러 전역에서 에러를 핸들링할 수 있도록 @ControllerAdvice, @ExceptionHandler 를 작성한다.

 

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<CustomResponseEntity> myCustomException(CustomException ex) {
        CustomResponseEntity customResponseEntity = new CustomResponseEntity();

        ErrorCode errorCode = ex.getErrorCode();
        customResponseEntity.setCode = errorCode.getStatus().value();
        customResponseEntity.setMessage = errorCode.getMessage();

        return new ResponseEntity<>(customResponseEntity, errorCode.getStatus());
    }
}

 

특정 코드에 에러 발생시키기.

 

throw new CustomException(EXPIRED_TOKEN);

// output
{
    "code": 401,
    "message": "Expired Token"
}

 

CustomException(EXPIRED_TOKEN) 이라는 에러를 발생시키면, @ExceptionHandler 에서 해당 예외를 처리하는 예제이다. CustomException 을 제외한 예외들은 3번에서 작성한 CustomErrorController 에 의해 처리될 것이다. 3번 5번 조합!

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

트랙백  0 , 댓글  0개가 달렸습니다.
secret