EKS XRay 적용하기 (Java, SpringBoot, EKS, Xray)


AWS XRay

AWS XRay 서비스는 어플리케이션 추적 서비스다. 간단하게는 Application에 들어오는 요청들에 대해서 추적을 해주는 서비스다. Application에는 Agent가 설치되는 것이며 XRay 서버는 별도로 설치하여 AWS XRay서비스를 이용할 수 있다.

[1] Use a Filter to instrument incoming HTTP requests. When you add the X-Ray servlet filter to your application, the X-Ray SDK for Java creates a segment for each sampled request. This segment includes timing, method, and disposition of the HTTP request. Additional instrumentation creates subsegments on this segment.

따라서, AWS XRay를 사용하기 위해서는 간단하게 아래와 같은 설정들이 필요하다.

  1. AWS XRay를 사용하기 위해서는 Application에 XRay agent(Libraries)를 적용한다.
  2. XRay 서버를 설치
  3. 권한설정

여기서 설명할 구조는 Pod안에 XRay가 설정된 Application이 있고, Sidecar Pattern으로 사용을 xray-daemon을 띄운다. 그러면 같은 파드에서는 localhost통신이 가능하기 때문에 agent는 localhost:2000 UDP포트로 통신을 하기 때문에 별도의 네트워크 설정없이 xray-daemon에서 요청을 받아 처리한다.

XRay 권한 설정

Pod가 UDP포트로 수신하는 Daemon을 통해 XRay로 segments를 보내면 X-Ray daemon은 큐에 쌓았다가 XRay로 배치 업로드한다.
이 때 X-Ray에 접근할 수 있는 권한을 셋팅해주어야 하며 Pod의 ServiceAccount에 설정된 권한에 따라 작동한다.

먼저 ServiecAccount에 설정할 권한을 셋팅한다.

AWSEKSXrayRole

CreateRole을 하여 AWSEKSXrayRole을 만든다. Policy는 AWSXrayFullAccess 권한을 주며 Trust Relationships를 아래와 같이 셋업한다.
Federated값은 arn:aws:iam::111122223333:oidc-provider/EKS_OpenID_Connect_Provider_URL 값이다.
StringEquals에 oidc:sub에 들어가는 값으로는 사용할 serviceaccount를 적어주면 된다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/111122223333DAB9000C5839D1829CA11"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.ap-northeast-2.amazonaws.com/id/111122223333DAB9000C5839D1829CA11:aud": "sts.amazonaws.com",
          "oidc.eks.ap-northeast-2.amazonaws.com/id/111122223333DAB9000C5839D1829CA11:sub": "system:serviceaccount:<namespace>:<serviceaccount_name>",
        }
      }
    }
  ]
}


Application을 위한 ServiceAccount를 별도 생성하지 않았다면 Application이 존재하는 namespace의 default ServiceAccount로 동작하며 아래 명령어를 통해 annotaions에 role을 추가한다.

kubectl edit sa default -n namespace
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/AWSEKSXrayRole
......


권한설정을 한 후 데몬을 띄우면 정상적으로 아래와 같은 로그 메세지가 보인다.

[Info] Successfully sent batch of 1 segments 
[Info] Successfully sent batch of 1 segments 
[Info] Successfully sent batch of 1 segments 


XRay daemon 설정

X-Ray Daemon X-Ray를 띄우는 방식은 WorkerNode에 Daemon으로 띄울수도 있고, Pod에 Sidecar Pattern으로 띄울 수도 있다.
이번에 적용한 것은 Pod에 Sidecar Pattern으로 Container를 하나 더 띄워 진행하는 것으로 하였다.

아래와 같이 deployment.yaml에 기존 main-api 컨테이너 이외의 Sidecar형식으로 xray-daemon 컨테이너를 적용하였다.

containers:
  - name: xray-daemon
    image: amazon/aws-xray-daemon
    imagePullPolicy: Always
    ports:
      - containerPort: 2000
        name: xray
        protocol: UDP
    resources:
      requests:
        cpu: 50m
        memory: 50Mi
    env:
      - name: AWS_REGION
        value: "ap-northeast-2"

XRay Local daemon 설정

X-Ray를 로컬 환경에서 테스트 할 수도 있다. 개발 단계에서는 Local 환경에 Daemon을 Download하여 실행시키고 테스트가 가능하다.

./xray_mac -o -n ap-northeast-2


Application 설정

  • Env
    • Spring Boot 2.7.5
    • Gradle 7.5
    • XRay com.amazonaws:aws-xray-recorder-sdk-bom:2.13.0
    • EKS 1.24

XRay for AWS SDK

build.gradle
다른 AWS SDK를 계측하기 위해서는 아래 문서와 같이 aws-xray-recorder-sdk-aws-sdk-v2-instrumentor를 제거하고 aws-xray-recorder-sdk-aws-sdk-v2 모듈을 추가한다. Tracing AWS SDK calls with the X-Ray SDK for Java

dependencyManagement {
    imports {
        mavenBom('com.amazonaws:aws-xray-recorder-sdk-bom:2.13.0')
    }
}
dependencies {
   // XRay
    implementation("com.amazonaws:aws-xray-recorder-sdk-spring")
    implementation("org.springframework.boot:spring-boot-starter-aop")
    implementation("com.amazonaws:aws-xray-recorder-sdk-core")
    implementation("com.amazonaws:aws-xray-recorder-sdk-aws-sdk")
//    implementation("com.amazonaws:aws-xray-recorder-sdk-aws-sdk-v2-instrumentor")
    implementation("com.amazonaws:aws-xray-recorder-sdk-aws-sdk-v2") // v2-instrumentor 대체
    implementation("com.amazonaws:aws-xray-recorder-sdk-slf4j")
    implementation("com.amazonaws:aws-xray-recorder-sdk-apache-http")
    implementation("com.amazonaws:aws-xray-recorder-sdk-sql-mysql")
}

XRayConfig.java

package com.bys.aws.main.config;

@Slf4j
@Configuration
public class XRayConfig {

    @Value("${xray.name}")
    private String XRAY_APP_NAME;

    static {
        AWSXRayRecorderBuilder builder = AWSXRayRecorderBuilder
                .standard()
                .withSegmentListener(new SLF4JSegmentListener("bys-xray"))
                .withPlugin(new EC2Plugin())
                .withPlugin(new EKSPlugin());

        URL ruleFile = XRayConfig.class.getResource("/awssdk-xray-rules.json");
        log.debug("xrayRuleFile: " + ruleFile.getFile());
        builder.withSamplingStrategy(new CentralizedSamplingStrategy(ruleFile));
        AWSXRay.setGlobalRecorder(builder.build());
    }

    @Bean
    public Filter TracingFilter() {
        log.debug("The segment name for aws xray tracking has been set to {}.", XRAY_APP_NAME);
        return new AWSXRayServletFilter(
                Optional.ofNullable(XRAY_APP_NAME).orElseThrow()
        );
    }
}

XRayInspector.java

package com.bys.aws.main.config;

@Slf4j
@Aspect
@Component
public class XRayInspector extends BaseAbstractXRayInterceptor {

    @Override
    @Pointcut("@within(com.amazonaws.xray.spring.aop.XRayEnabled) && (bean(*Controller) || bean(*Service) || bean(*Client))")
    public void xrayEnabledClasses() {}

    @Override
    protected Map<String, Map<String, Object>> generateMetadata(ProceedingJoinPoint proceedingJoinPoint, Subsegment subsegment) {
        return super.generateMetadata(proceedingJoinPoint, subsegment);
    }
}

resources/awssdk-xray-rules.json

{
  "version": 2,
  "rules": [
    {
      "description": "awssdk-storage-dev",
      "host": "*",
      "http_method": "*",
      "url_path": "/storage/*",
      "fixed_target": 0,
      "rate": 0.5
    }
  ],
  "default": {
    "fixed_target": 1,
    "rate": 0.1
  }
}

S3Client 예시
AWS SDK Java 2.X 부터는 overrideConfiguration을 통해 TracingInterceptor를 추가한다.

package com.bys.aws.main.config;

@Slf4j
@Component
public class AWSSDKConfig {

    Region region = Region.AP_NORTHEAST_2;

    @Bean
    public S3Client getStsClient()
    {
        return S3Client.builder()
                .overrideConfiguration(ClientOverrideConfiguration.builder()
                        .addExecutionInterceptor(new TracingInterceptor())
                        .build())
                .region(region)
                .build();
    }
}


기본적으로 데몬은 0.0.0.0:2000 포트로 Listen을 하고 있으며, Application에서는 AWS SDK를 적용하여 AWSXRayServletFilter를 셋업하면 localhost:2000 포트로 Segment를 전달하게 되어있다.
aws-xray-recorder-sdk-core/DaemonConfiguration.class 파일에서는 DEFAULT_ADDRESS=”127.0.0.1:2000”으로 셋팅되어있는 것을 확인할 수 있다.

이런 값들은 환경변수, 시스템변수를(com.amazonaws.xray.emitters.daemonAddress or AWS_XRAY_DAEMON_ADDRESS) 통해 변경할 수 있으며 DAEMON_ADDRESS 외에도 다른 환경변수 값을 통해 변경할 수 있는 값들이 존재한다.

xray_awssdk.png


XRay for outgoing request

Http요청에 대한 XRay를 테스트 하기 위해서 위 그림에서 bys-awssdk-storage-dev 어플리케이션에서 bys-awssdk-iam-dev 어플리케이션을 호출하여 현재 호출하고 있는 IAM Identity를 가져오기 위한 작업을 진행했다.
문제는 WebClient를 사용하여 Http 요청을 하였으나 위 그림 상에서 추적이 되지 않는 현상이 발생 되었다. (예상 호출 그림: bys-awssdk-storage-dev -> appmesh(bys-awssdk-iam-dev)) 확인을 해보니 현재는 Apache clients를 이용한 HTTP 요청만을 지원하고 있었다.
따라서, FeignClient로 변경했다.

XRayFeignClientConfig

import com.amazonaws.xray.proxies.apache.http.HttpClientBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class XRayFeignClientConfig {

    @Bean
    public CloseableHttpClient feignClient() {
        CloseableHttpClient httpclient = HttpClientBuilder.create().build();
        return httpclient;
    }
}

StsFeignClient

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.HashMap;

@FeignClient(name = "sts-api", url = "${feign.apiurl.iam}")
public interface StsFeignClient {

    @GetMapping(value="/v2/sts/id", produces = "application/json", consumes = "application/json")
    public HashMap<String, String> getCallerId();
}

S3ServiceV2

@Slf4j
@Service
@RequiredArgsConstructor
public class S3ServiceV2 {

    private final S3Client s3Client;
    private final StsFeignClient stsFeignClient;

    public List<BucketModelV2> getBuckets() {
        HashMap<String, String> iamResponse = stsFeignClient.getCallerId();
        log.info("userId: " + iamResponse.get("userId"));
        log.info("account: " + iamResponse.get("account"));
        log.info("arn: " + iamResponse.get("arn"));

        List<BucketModelV2> buckets = new ArrayList<>();
        try {
            ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build();
            ListBucketsResponse listBucketsResponse = s3Client.listBuckets(listBucketsRequest);
            listBucketsResponse.buckets().stream().forEach(x -> {
                buckets.add(new BucketModelV2(x.name(), x.creationDate()));
            });
        } catch (S3Exception e) {
            log.error(e.awsErrorDetails().errorMessage());
        }
        return buckets;
    }

    //convert bytes to kbs.
    private static long calKb(Long val) {
        return val/1024;
    }
}

application-dev.yaml

# Feign Setting
feign:
  httpclient:
    enabled: true
  client:
    config:
      sts-api: # Feign Client 명
        decode404: false
        loggerLevel: full
        connect-timeout: 3000
        readTimeout: 60000
  apiurl:
    ec2: http://awssdk-ec2-dev-svc.aws:10010/ec2
    storage: http://awssdk-storage-dev-svc.aws:10011/storage
    iam: http://awssdk-iam-dev-svc.aws:10012/iam






  • References
    [1] Tracing incoming requests - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-filters.html
    [2] X-Ray SDK for Java - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java.html
    [3] X-Ray SDK for Java Clients - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-awssdkclients.html
    [4] WebClient Issue - https://github.com/aws/aws-xray-java-agent/issues/87
    [5] Github - spring-boot-aws-xray-sample - https://github.com/anthunt/spring-boot-aws-xray-sample
    [6] XRay Daemon Local - https://docs.aws.amazon.com/xray/latest/devguide/xray-daemon-local.html#xray-daemon-local-linux

Tag: [ aws  eks  xray  ]