레이블이 computer인 게시물을 표시합니다. 모든 게시물 표시
레이블이 computer인 게시물을 표시합니다. 모든 게시물 표시

[컴] Remix vs Next.js

리믹스

Remix vs Next.js

Remix 쪽에서 자세히 Next.js 와 자신의 차이를 설명해 준다.

개인적으로 Nextjs 의 복잡함을 좋아하지 않는데, Remix 가 좋은 대안을 주는 듯 하다.

대략 기억나는 것을 정리중…

  • Next.js 보다 빠르다.
  • HTTP stale-while-revalidate caching directive 를 이용한다.
  • Next.js 가 느린 이유중 하나는 SSG(static site generation)을 사용할 수 없는 화면인 경우 Next.js 는 브라우저에서 서버로 검색결과를 조회하고, 그 data 를 보고 이미지를 가져와야 해서이다. 더구나 서버로 검색결과를 조회하는 시점도 브라우저가 최초의 javascript 를 받은 후 처리하고 나서야 시작하게 된다. (network waterfall request chain)
  • Nextjs 가 data 를 가져오는데 4가지 모드를 제공하는데, Remix는 1개의 모드(loader)를 제공.

[컴] 확장성, 유지보수성, Scalability and Maintainability

Scalability and Maintainability

ref. 1 의 Chapter 1, Reliable, Scalable, and Maintainable Applications

확장성 Scalability

성능에 대한 표현

Scalability > Describing Performance

p.14

  • 응답시간의 평균은 유저들이 일반적으로 어떤 평균 응답시간을 경험했는지를 말해주지 못한다.
  • 이때는 percentiles(백분위수)를 사용하는 것이 낫다.
  • 중앙값(median)은 50번째 백분위수라서 p50 으로 불린다.
  • 중앙값이 1.5ms 라고 하면, 절반은 1.5ms 보다 느렸고, 절반은 1.5ms 보다 빨랐다고 알 수 있다.

p. 15

  • 예를 들어 Amazon은 내부 서비스에 대한 응답 시간 요구 사항을 99.9% 라고 설명한다. 이말은 1,000건의 요청 중 1건에만 영향을 미친다는 뜻이다. 이는 요청이 가장 느린 고객이 종종 계정에 가장 많은 데이터를 보유하고, 구매횟수가 많은 고객이기 때문이다. 즉 가장 가치있는 고객들이다.[19].
  • 반면에 99.99번째 백분위수(요청 10,000건 중 가장 느린 1건)를 최적화하는 것은 비용이 너무 많이 들고 아마존의 목적에 비해 충분한 이점을 얻지 못하는 것으로 간주되었다.
  • 응답 시간이 100밀리초 증가하면 매출이 1% 감소하고[20], 1초 느려지면 고객 만족 지표가 16% 감소한다는 보고도 있다.[21, 22]

p. 16

  • 응답 시간 데이터를 집계하는 올바른 방법은 히스토그램을 추가하는 것
  • 시간 해상도를 낮추거나 여러 머신의 데이터를 결합하기 위해 백분위수를 평균하는 것은 수학적으로 의미가 없다.

부하 다루기

Scalability > Approaches for Coping with Load

  • scale up(수직확장, 더 강력한 머신으로 이동)
  • scale out(수평확장, 여러 대의 작은 머신에 부하를 분산)
  • 탄력적 시스템(elastic system)은 부하를 예측할 수 없는 경우에 유용할 수 있다. 하지만 수동으로 확장하는 시스템이 더 간단하고 운영상의 돌발 상황이 적을 수 있다.


  • ’단일 노드로 된 상태 저장 데이터 시스템(stateful data system)’을 분산 설정으로 전환하면 많은 복잡성이 추가될 수 있다.
  • 일반적인 방법은 ‘확장 비용(scaling cost)’이나 ’고가용성 요건(high availability requirements)’ 들이 데이터베이스를 분산하는 것이 나은 시점이 올때까지 단일 노드에 데이터베이스를 유지(스케일업)하는 것.
  • 그러나 요즘 분산시스템을 구성하기가 더 편리해져서 향후, 대량의 데이터나 트래픽을 처리하지 않는 사용사례에서도 분산형 데이터 시스템이 기본이 될 수 있다.


  • 대규모로 운영되는 시스템의 아키텍처는 일반적으로 애플리케이션에 따라 매우 특수하다.
  • 그래서 모든 경우에 사용가능한 확장 가능한 아키텍처(비공식적으로 마법의 확장 소스라고 알려진)와 같은 것은 존재하지 않는다.
  • 특정 응용을 위해 잘 확장되는 구조는 어떤 작업이 일반적이고 어떤 작업이 드문지에 대한 가정, 즉 부하 매개변수(load parameters)를 기반으로 구축된다. 이러한 가정이 잘못된 것으로 판명되면, scaling 을 위한 노력은 가장 최악의 낭비가 되어 버린다.
  • 그래서 초기스타트업이나, 그렇게 흥행이 되지 않은 제품에서는 미래 부하량에 맞춰서 확장하는 것보단 제품 기능들을 빠르게 재시작(iterate)할 수 있는 것이 더 중요하다.

Maintainability

  • 소프트웨어 비용의 대부분은 지속적인 유지 보수에 발생
    • 버그 수정
    • 시스템 운영 유지
    • 장애 조사
    • 새로운 플랫폼에 맞게 조정
    • 새로운 사용 사례에 맞게 수정
    • 기술 부채 상환
    • 새로운 기능 추가
  • 유지보수시 고통을 최소화하도록 sw를 설계, 그래야 legacy sw 가 되지 않는다.
  • 이를 위한 3가지 설계원칙, 이 원칙을 염두에 두고 시스템을 고민해야 한다.
    • 운영성(Operability) : 운영팀이 쉽게 운영할 수 있도록
    • 단순성(Simplicity) : 새로운 엔지니어가 시스템을 쉽게 이해할 수 있도록
    • 진화가능성(Evolvability) : 확장성(extensibility), 수정가능성(modifiability), 가소성(plasticity) 라고도 부른다.

운영성

  • 좋은 운영성은 ‘일상적인 작업(routine task)을 쉽게 만드는 것’ 그래서 운영팀이 고부가가치 활동에 집중할 수 있도록 하는 것을 의미
  • 좋은 운영팀이 중요
  • ’좋은 운영팀’이란
    • 시스템 상태 모니터링 및 불량 상태 발생 시 신속한 서비스 복구
    • 시스템 장애 또는 성능 저하와 같은 문제의 원인을 추적합니다.
    • 보안 패치를 포함한 소프트웨어와 플랫폼을 최신 상태로 유지
    • 서로 다른 시스템이 서로에게 어떤 영향을 미치는지 파악하여 문제가 되는 변경이 손상을 일으키기 전에 피할 수 있도록 유지보수성 확보
    • 향후 문제를 예측하고 문제가 발생하기 전에 해결(예: 용량 계획)
    • 배포, 구성 관리 등을 위한 모범 사례 및 도구 설정
    • 애플리케이션을 한 플랫폼에서 다른 플랫폼으로 이동하는 것과 같은 복잡한 유지 관리 작업 수행
    • 구성이 변경될 때 시스템의 보안 유지
    • 운영을 예측 가능하게 하고 프로덕션 환경을 안정적으로 유지하는 데 도움이 되는 프로세스 정의
    • 개별 직원이 이동하는 경우에도 시스템에 대한 조직의 지식 유지

routine task 를 쉽게 만드는 방법 :

  • 우수한 모니터링을 통해 런타임 동작 및 시스템 내부에 대한 가시성 제공
  • 자동화 및 표준 도구와의 통합을 위한 우수한 지원 제공
  • 개별 머신에 대한 종속성 방지(시스템 전체가 중단 없이 계속 실행되는 동안 유지보수를 위해 머신을 중단할 수 있음)
  • 좋은 문서이해하기 쉬운 운영 모델 제공(“X를 하면 Y가 발생합니다”)
  • 좋은 기본 동작을 제공하되, 관리자가 필요할 때 기본값을 재정의할 수 있는 자유를 제공해야 합니다.
  • 적절한 경우 자가 복구 기능을 제공하되, 필요한 경우 관리자가 시스템 상태를 수동으로 제어할 수 있어야 합니다.
  • 예측 가능한 동작을 보여줌으로써 돌발 상황 최소화

단순성 Simplicity

  • 시스템을 단순하게 만드는 것은 accidential complexity 를 줄이는 것을 뜻하기도 한다.
  • accidentail complexity 는 소프트웨어가 해결하려고 하는 문제에서 나온 복잡성이 아니라, 단순히 구현에서 나온 복잡성이라고 정의
  • 이것을 줄이는 좋은 방법중 하나가 추상화(abstraction) 이다.


  • 복잡하고 이해하기 어려운 시스템은 시스템작업과 연관된 모든 사람의 작업속도를 떨어뜨린다.
  • 복잡하고 이해하기 어려운 시스템은 유지비용을 증가시킨다.
  • 복잡성 –> 유지보수 어려움 –> 예산, 일정의 초과
  • 복잡성의 다양한 증상
    • state space 의 폭발
    • 모듈의 긴밀한 결합(tight coupling)
    • 얽힌 종속성
    • 일관되지 않은 명명 및 용어
    • 성능문제를 해결하기 위한 hacks
    • 다른 곳의 이슈를 해결하기 위한 특수케이스
  • 변경시 버그가 발생할 위험이 더 크다.
    • 개발자가 시스템을 이해하기 어렵고, 이유를 찾기 어렵다.
    • 복잡한 시스템에선 숨겨진 가정, 의도하지 않은 결과, 예기치 않은 상호작용을 더 간과하기 쉽다.

진화가능성, 변경을 쉽게 Evolvability

  • 데이터 시스템을 수정하는 것과 변화하는 요구 사항에 맞게 조정하는 것을 쉽게 하려면 단순성 및 추상화가 중요하다.
  • 단순하고 이해하기 쉬운 시스템이 수정하기 더 쉽다.

Reference

  1. Designing Data Intensive Applications


[컴] Domain Driven Design(DDD)

 

Domain Driven Design(DDD)

왜 DDD 를 이야기 하는가?

위의 글, Introduction into Domain-Driven Design (DDD), 에 있는 인용문을 보면 DDD 가 말하고자 하는 것을 알 수 있다.

  • 코드에 현실세계를 반영하는 것. 우리의 시스템을 modeling 하는 더 나은 방법이다.

from : Building Microservices by Sam Newman

Eric Evan’s book DDD helped us understand the importance of representing the real world in our code. and showed us better ways to model our systems.

나의 정리

이해한 바를 정리하면, 우리가 우리의 시스템을 우리의 생각대로 그려나갈 수 있지만, 그 그림이 현실세계와 맞닿아 있을때 더 낫다. 그리고 그렇게 시스템을 디자인 하는 것이 DDD 이다.

See Also 4. 를 참고하면, 구체적으로 이 디자인이 어떤식으로 구현되는지를 알 수 있다.

하지만 See Also 5. 의 이야기처럼, DDD 의 기본개념이 뜻하는 바를 이해하는 것이 중요하다. 무조건 DDD에서 제안하는 pattern 이나 기술규칙을 사용하는 것이 DDD가 아니다.

from : Designing a DDD-oriented microservice - .NET | Microsoft Learn

때때로 이러한 DDD 기술 규칙과 패턴은 DDD 접근 방식(DDD aproaches)을 구현하는 데 있어 가파른 학습 곡선을 가진 장애물로 인식되기도 합니다. 하지만 중요한 부분은 패턴 자체가 아니라 코드를 비즈니스 문제에 맞게 구성하고 동일한 비즈니스 용어(유비쿼터스 언어)를 사용하는 것입니다. 또한 DDD 접근 방식(DDD aproaches)은 중요한 비즈니스 규칙이 있는 복잡한 마이크로서비스를 구현하는 경우에만 적용해야 합니다. CRUD 서비스와 같이 더 단순한 책임은 더 간단한 접근 방식으로 관리할 수 있습니다.

Sometimes these DDD technical rules and patterns are perceived as obstacles that have a steep learning curve for implementing DDD approaches. But the important part is not the patterns themselves, but organizing the code so it is aligned to the business problems, and using the same business terms (ubiquitous language). In addition, DDD approaches should be applied only if you are implementing complex microservices with significant business rules. Simpler responsibilities, like a CRUD service, can be managed with simpler approaches.

See Also

  1. What is Domain Driven Design (DDD)? - Stack Overflow
  2. What is Domain Driven Design? - Stack Overflow
  3. Domain Driven Design for Services Architecture | Thoughtworks
  4. Entities and Value Objects: Diving Deep into Domain-Driven Design
  5. Designing a DDD-oriented microservice - .NET | Microsoft Learn

Domain Driven Design(DDD)

왜 DDD 를 이야기 하는가?

위의 글, Introduction into Domain-Driven Design (DDD), 에 있는 인용문을 보면 DDD 가 말하고자 하는 것을 알 수 있다.

  • 코드에 현실세계를 반영하는 것. 우리의 시스템을 modeling 하는 더 나은 방법이다.

from : Building Microservices by Sam Newman

Eric Evan’s book DDD helped us understand the importance of representing the real world in our code. and showed us better ways to model our systems.

나의 정리

이해한 바를 정리하면, 우리가 우리의 시스템을 우리의 생각대로 그려나갈 수 있지만, 그 그림이 현실세계와 맞닿아 있을때 더 낫다. 그리고 그렇게 시스템을 디자인 하는 것이 DDD 이다. 

See Also 4. 를 참고하면, 구체적으로 이 디자인이 어떤식으로 구현되는지를 알 수 있다.

See Also

  1. What is Domain Driven Design (DDD)? - Stack Overflow
  2. What is Domain Driven Design? - Stack Overflow
  3. Domain Driven Design for Services Architecture | Thoughtworks
  4. Entities and Value Objects: Diving Deep into Domain-Driven Design

[컴] web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

cors 설정 / redirect fetch / fetch cors

web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

  • 다음 sequence diagram 은 만약 내가 site.a.com 에 접속한 상황에서 fetch 로 api.a.com 에 request 를 했다. 그 상황에서 api.a.com 가 redirect site.a.com 를 나에게 던져준 경우이다.
  • 이 경우 예상치 못하게 cors error 가 떴다.

CORS error 가 뜨는 이유

CORS error 가 뜨는 이유는 Origin 이 null 로 set 돼서 site.a.com 로 request 를 보내기 때문이다.

  • 여기를 보면, 처음 request 를 보낸 url에서 바뀐적이 있으면, redirect-tainted origin 으로 본다. 그렇기에 Origin 이 null 로 잡힌다.
  • 이러면 origin 이 ‘null’ 로 바뀌었다고 봐야 하기 때문에 site.a.com 에서 CORS 를 지원하기 위한 설정을 잡아줘야 한다.

다음처럼 api.a.com 으로 보내고 api.a.com 으로 redirect 되는 경우는 다음처럼 CORS error 가 안뜨고, 잘 동작한다.

api.a.com, api.a.com

CORS error 가 안뜨게 수정

  • 아래는 이때 CORS error 가 뜨지 않도록 header 를 맞춰준 예시이다.

조정한 내용:

  • Accee-Control-Allow-Origin: null

  • Access-Control-Allow-Credentials: true

  • Access-Control-Allow-HeadersAccess-Control-Request-Headers 에서 요청한 header를 넣어준다.

  • Access-Control-Allow-MethodsAccess-Control-Request-Method에 맞춰서 지정

  • api.a.com 으로 보내고 site.a.com 으로 redirect 되는 경우 : mermaid link

api.a.com, site.a.com

fetch 를 사용할 때 redirect :

CORS error 가 안뜨게 하려면

CORS error 가 안뜨게 하려면, 다음처럼 설정을 하면 된다.

client :

  • `credentials : ‘omit’
  • mode : 'no-cors'

server :

  • Access-Control-Allowed-Origin: *
  • Access-Control-Allow-Credentials: false

Fetch: Cross-Origin Requests

  • client 에서 Origin을 보내면, 그것을 server가 본다.
  • server 는 그에 따라 Access-Control-Allow-Origin 을 만들어서 response 에 보낸다.
  • browser 가 trusted mediator(신뢰할 수 있는 중재자) 역할을 한다. 즉, browser가 이제 client 에서 보냈던 origin 이랑 서버가 보낸 Access-Control-Allow-Origin 값을 비교해서 값이 적절하면, javascript 가 response 값을 access 할 수 있게 해준다.

javascript 가 기본적으로 접근할 수 있는 safe header 는 다음과 같다.

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

js 가 다른 header 에 접근하고 싶어도 browser가 접근을 막는다. 다만 server 에서 Access-Control-Expose-Headers 에 추가된 header는 browser가 js 한테 열어준다.

이제는 browser가 안전하지 않은 요청(unsafe request)는 바로 날리지 않는다. preflight request 를 먼저 날리고 나서 날리게 된다.

  • Access-Control-Request-Method : 어떤 method 로 보내려는지 적는다.
  • Access-Control-Request-Headers : 보내려는 request 가 갖고 있는 header들을 적는다.
  • Origin : request 가 어디서 보내는 request 인지를 적게 된다.
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
Content-Encoding: gzip
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Encoding,API-Key

서버단에서 redirect 시켜서 CORS 피하기

  • preflight 등의 성능을 방해하는 요소를 줄일 수 있다.
                              ┌─►  http://localhost:8081       
   http://localhost:8080      │     ┌─────────────┐            
                              │     │             │            
   http://localhost:8080/api──┘     │   WAS       │            
     ┌─────────────┐                │             │            
     │ web server  │                │             │            
     │     ┌───────┤                │             │            
     │     │       │                └─────────────┘            
     │     │ asset │                                           
     │     │       │                                           
     └─────┴───────┘                                           

[컴][웹] Http Header Origin 이 null 인 경우

spec , origin이 널 / null 인 경우 / why origin null

Http Header Origin 이 null 인 경우

origin 값은 opaque origin 또는 tuple origin 인데, tuple origin 이 정상적인 값으로 ‘http://daum.net’ 같은 값이고, opaque origin (이해하기 힘든 origin)은 null 이라고 보면 된다.

The serialization of ("https", "xn--maraa-rta.example", null, null) is "https://xn--maraa-rta.example".

아래 링크에서 origin 이 null 이 되는 모든 case 를 이야기 해준다.

fetch 를 A에 했는데, B 로 redirect 되는 경우 Origin 은 null

fetch 를 한 경우 다른 origin 으로 redirect 하면 null 이 된다.

See Also

  1. 쿠…sal: [컴] web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

[컴] 쿠팡, 판매상품의 재고가 0으로 표시되던 현상, redis 이슈

 에러 / 장애 / 캐시 / 레디스 이슈 / 레디스 버그 /

쿠팡, 판매상품의 재고가 0으로 표시되던 현상, redis 이슈

장애

  • 2019년 7월 24일 오전 7시경부터 쿠팡 판매 상품의 재고가‘0’으로 표시돼, 소비자는 관련 상품의 주문 및 구매할 수 없었다.

장애원인

쿠팡에서 직접적으로 밝히진 않았지만, 장애원인으로 지목되는 것은 다음 이슈이다.

간략하게 이야기하면 item 을 추가할 때 쓰는 dictAddRaw 함수의 버그이다. long long(64bit) 으로 사용해야 할 index를 int 로 사용해서 문제가 발생했다.

Reference

  1. 쿠팡 오류 원인은 오픈소스 ‘레디스 DB’ 때문 < 게임·인터넷 < 기사본문 - 디지털투데이 (DigitalToday), 2019-07-24
  2. 쿠팡 오류 원인은 오픈소스 ‘레디스 DB’ 때문 : 클리앙

[컴] Spring 의 securityWebFilterChain 에서 filter 가 추가되는 과정

스프링 / springboot / 스프링부트 / security

Spring 의 securityWebFilterChain 에서 filter 가 추가되는 과정

  • WebFilterChainProxy.filter()에서 WebFluxSecurityConfiguration.securityWebFilterChains 을 돌면서 실행한다.
  • WebFilterChainProxy@Bean WebFluxSecurityConfiguration.springSecurityWebFilterChainFilter 에 의해서 new 된다.
  • securityWebFilterChains@Autowire setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterChains) 에 의해 new 된다.
  • WebFluxSecurityConfiguration@EnableWebFluxSecurity 에 의해 불려진다.
  • @EnableWebFluxSecuritySecurityConfig 에 설정돼 있다.
  • @Bean SecurityConfig.securityWebFilterChain 에서 filter를 정하고, ServerHttpSecurity.build() 를 호출하는데, 이 때 filter들이 실제로 추가된다.
    • formLogin 인 경우 login url 인 ‘/login’ 도 설정된다.

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig
    @Bean
    SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
            ...
            .authorizeExchange(exchange -> exchange
                    ...
                    .and()
                    .formLogin(formLogin -> formLogin
                            .authenticationSuccessHandler(authenticationSuccessHandler)
                            .authenticationFailureHandler(authenticationFailureHandler)
                    )
            );
        return http.build();    // ServerHttpSecurity.build
    }

public class ServerHttpSecurity
    private FormLoginSpec formLogin;
    public SecurityWebFilterChain build() {
        if (this.formLogin != null) {
            if (this.formLogin.authenticationManager == null) {
                this.formLogin.authenticationManager(this.authenticationManager);
            }
            if (this.formLogin.securityContextRepository != null) {
                this.formLogin.securityContextRepository(this.formLogin.securityContextRepository);
            }
            else if (this.securityContextRepository != null) {
                this.formLogin.securityContextRepository(this.securityContextRepository);
            }
            else {
                this.formLogin.securityContextRepository(new WebSessionServerSecurityContextRepository());
            }
            // add filter to the this.webFilters
            this.formLogin.configure(this);
        }
        ...
        AnnotationAwareOrderComparator.sort(this.webFilters);
        List<WebFilter> sortedWebFilters = new ArrayList<>();
        this.webFilters.forEach((f) -> {
            if (f instanceof OrderedWebFilter) {
                f = ((OrderedWebFilter) f).webFilter;
            }
            sortedWebFilters.add(f);
        });
        sortedWebFilters.add(0, new ServerWebExchangeReactorContextWebFilter());

        return new MatcherSecurityWebFilterChain(getSecurityMatcher(), sortedWebFilters);
    }

    public final class FormLoginSpec {
        protected void configure(ServerHttpSecurity http) {
            if (this.authenticationEntryPoint == null) {
                this.isEntryPointExplicit = false;
                loginPage("/login");
            }
            ...
        }
    }


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ ServerHttpSecurityConfiguration.class, WebFluxSecurityConfiguration.class,
        ReactiveOAuth2ClientImportSelector.class })
@Configuration
public @interface EnableWebFluxSecurity {

}

@Configuration(proxyBeanMethods = false)
class WebFluxSecurityConfiguration {

    private List<SecurityWebFilterChain> securityWebFilterChains;

    ...

    @Autowired(required = false)
    void setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterChains) {
        // SecurityConfig.securityWebFilterChain()
        this.securityWebFilterChains = securityWebFilterChains;
    }

    @Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
    @Order(WEB_FILTER_CHAIN_FILTER_ORDER)
    WebFilterChainProxy springSecurityWebFilterChainFilter() {
        return new WebFilterChainProxy(getSecurityWebFilterChains());
    }

    private List<SecurityWebFilterChain> getSecurityWebFilterChains() {
        List<SecurityWebFilterChain> result = this.securityWebFilterChains;
        if (ObjectUtils.isEmpty(result)) {
            return Arrays.asList(springSecurityFilterChain());
        }
        return result;
    }


public class WebFilterChainProxy implements WebFilter {
    private final List<SecurityWebFilterChain> filters;

    public WebFilterChainProxy(List<SecurityWebFilterChain> filters) {
        this.filters = filters; // WebFluxSecurityConfiguration.securityWebFilterChains
    }
    ...
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return Flux.fromIterable(this.filters)
                .filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(exchange)).next()
                .switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
                .flatMap((securityWebFilterChain) -> securityWebFilterChain.getWebFilters().collectList())
                .map((filters) -> new FilteringWebHandler(chain::filter, filters)).map(DefaultWebFilterChain::new)
                .flatMap((securedChain) -> securedChain.filter(exchange));  // securedChain is `DefaultWebFilterChain`
    }
    ...
}
@AutoConfiguration(after = { WebFluxAutoConfiguration.class })
@ConditionalOnClass({ DispatcherHandler.class, HttpHandler.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnMissingBean(HttpHandler.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class HttpHandlerAutoConfiguration {
    @Configuration(proxyBeanMethods = false)
    public static class AnnotationConfig {

        ...

        @Bean
        public HttpHandler httpHandler(ObjectProvider<WebFluxProperties> propsProvider) {
            HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(this.applicationContext).build();
            ...
            return httpHandler;
        }

    }

}

public final class WebHttpHandlerBuilder{
    public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
        ...
        // set this.filters
        List<WebFilter> webFilters = context
                .getBeanProvider(WebFilter.class)
                .orderedStream()
                .collect(Collectors.toList());
        builder.filters(filters -> filters.addAll(webFilters));
    }
    public HttpHandler build() {
        WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
        decorated = new ExceptionHandlingWebHandler(decorated,  this.exceptionHandlers);

        HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
        ...
        // return adapted
        return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted);
    }
}

public class FilteringWebHandler extends WebHandlerDecorator 
    public FilteringWebHandler(WebHandler handler, List<WebFilter> filters) {
        super(handler);
        this.chain = new DefaultWebFilterChain(handler, filters);
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        return this.chain.filter(exchange);
    }

public class DefaultWebFilterChain implements WebFilterChain
    public DefaultWebFilterChain(WebHandler handler, List<WebFilter> filters) {
        Assert.notNull(handler, "WebHandler is required");
        this.allFilters = Collections.unmodifiableList(filters);
        this.handler = handler;
        DefaultWebFilterChain chain = initChain(filters, handler);
        this.currentFilter = chain.currentFilter;
        this.chain = chain.chain;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange) {
        return Mono.defer(() ->
                this.currentFilter != null && this.chain != null ?
                        invokeFilter(this.currentFilter, this.chain, exchange) :
                        this.handler.handle(exchange));
    }
    private Mono<Void> invokeFilter(WebFilter current, DefaultWebFilterChain chain, ServerWebExchange exchange) {
        String currentName = current.getClass().getName();
        return current.filter(exchange, chain).checkpoint(currentName + " [DefaultWebFilterChain]");
    }

requst 를 처리시점에 호출되는 filter()

  1. HttpWebHandlerAdapter에서 getDelegate().handle(exchange)
  2. ExceptionHandlingWebHandler.handle() 에서 super.handle() 을 호출
  3. FilteringWebHandler.handle()을 호출하게 된다.
  4. FilteringWebHandler.handle()에서 DefaultWebFilterChain.filter(exchange) 를 호출
public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler 
    @Override
    public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
        ...
        ServerWebExchange exchange = createExchange(request, response);
        ...
        return getDelegate().handle(exchange)
                .doOnSuccess(aVoid -> logResponse(exchange))
                .onErrorResume(ex -> handleUnresolvedError(exchange, ex))
                .then(Mono.defer(response::setComplete));
    }
public class ExceptionHandlingWebHandler extends WebHandlerDecorator 
    ...
    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        Mono<Void> completion;
        try {
            // WebHandlerDecorator.handle --> FilteringWebHandler.handle
            completion = super.handle(exchange);
        }
        catch (Throwable ex) {
            ...
        }

        for (WebExceptionHandler handler : this.exceptionHandlers) {
            completion = completion.onErrorResume(ex -> handler.handle(exchange, ex));
        }
        return completion;
    }

public class WebHandlerDecorator implements WebHandler

    private final WebHandler delegate;

    public WebHandlerDecorator(WebHandler delegate) {
        ...
        this.delegate = delegate;
    }
    ...

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        return this.delegate.handle(exchange);
    }

/login 호출

curl http://localhost:8888/login
  • DefaultWebFilterChain.filter
  • –> AuthenticationWebFilter.filter
  • --> AuthenticationWebFilter.authenticate()
  • --> authenticationManager.authenticate(token)
  • --> AbstractUserDetailsReactiveAuthenticationManager.authenticate

[컴] ChatGPT 에서 다른 사용자의 개인정보와 채팅제목이 노출되던 이슈, 2023-03-20

openAI 이슈 / 챗gpt /

ChatGPT 에서 다른 사용자의 개인정보와 채팅제목이 노출되던 이슈, 2023-03-20

2023년 3월 20일, chatgpt에서 특정사용자가 채팅기록 사이드바에서 다른 사용자의 대화에 대한 간략한 설명을 볼 수 있었다.(ref. 3)

redis-py library 의 이슈로 다른 사용자의 개인 정보와 채팅 제목이 노출되었다고 한다.

redis-py library 의 이슈

github issue 에 가면, 아래와 같은 재현 code를 확인할 수 있다.

import asyncio
from redis.asyncio import Redis
import sys


async def main():
    myhost, mypassword = sys.argv[1:]
    async with Redis(host=myhost, password=mypassword, ssl=True, single_connection_client=True) as r:

        await r.set('foo', 'foo')
        await r.set('bar', 'bar')

        t = asyncio.create_task(r.get('foo'))
        await asyncio.sleep(0.001)
        t.cancel()
        try:
            await t
            print('try again, we did not cancel the task in time')
        except asyncio.CancelledError:
            print('managed to cancel the task, connection is left open with unread response')

        print('bar:', await r.get('bar'))
        print('ping:', await r.ping())
        print('foo:', await r.get('foo'))


if __name__ == '__main__':
    asyncio.run(main())
$ python redis_cancel.py hostname password
managed to cancel the task, connection is left open with unread response
bar: b'foo'
ping: False
foo: b'PONG'

대략 그림으로 설명하면 이런 모습이다.

redis-py library 의 comuunicatino queue

위 그림에서 ’파란색’부분이 t에서 사용했던 부분인데, 이것이 connection 이 cancel 됐을때도 계속 남아있어서, 그 다음 command 인 r.get('bar') 를 send 할 때, r.get('foo') 가 대신 보내지는 것인 듯 하다. 그래서 결과가 하나씩 밀려서 나온다.

Reference

  1. OpenAI Reveals Redis Bug Behind ChatGPT User Data Exposure Incident, 2023-03-25
  2. Off by 1 - Canceling async Redis command leaves connection open, in unsafe state for future commands · Issue #2624 · redis/redis-py · GitHub
  3. ChatGPT Users Report Being Able to See Random People’s Chat Histories

[컴] node v20.x 에서 TLS v1.2 사용

node v20.x 에서 TLS v1.2 사용

댓글fetch, axios 에선 TLS v1.2 를 사용할 수 없다고 한다.

axios 는 모르겠지만, fetch 에선 TLS v1.2 로 통신하지 못했다. 그래서 https 를 사용해서 해결했다.

node, SSL_OP_LEGACY_SERVER_CONNECT

https.Agent

axios 나 fetch 나 이방법으로 해결이 가능하다.

const httpsAgent = new https.Agent({
  // for self signed you could also add
  // rejectUnauthorized: false,
  
  // allow legacy server
  secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
});
const response = await fetch(url, {
  agent: httpsAgent,
});

const html = await response.text();
console.log(html)

실패: secureOptions 없이 node-fetch 에서 https.Agent 를 사용

이 방법은 적어도 windows 에서는 동작하지 않았다.

import https from "https";
const agent = new https.Agent({
  rejectUnauthorized: false
});
fetch(myUrl, { agent });

아래와 같은 error 가 떴다.

reason: write EPROTO 382D0000:error:0A000152:SSL routines:final_renegotiate:unsafe legacy renegotiation disabled:c:\ws\deps\openssl\openssl\ssl\statem\extensions.c:922:

    at ClientRequest.<anonymous> (D:\a\prog\typescript\nextjs\budgetbuddy\budgetbuddybrain\node_modules\node-fetch\lib\index.js:1501:11)
    at ClientRequest.emit (node:events:518:28)
    at TLSSocket.socketErrorListener (node:_http_client:500:9)
    at TLSSocket.emit (node:events:518:28)
    at emitErrorNT (node:internal/streams/destroy:169:8)
    at emitErrorCloseNT (node:internal/streams/destroy:128:3)
    at processTicksAndRejections (node:internal/process/task_queues:82:21) {
  type: 'system',
  errno: 'EPROTO',
  code: 'EPROTO'
}
...

실패: https.request

처음에는 req.on('data', (chunk) => {}) 부분을 실행하지 않았어서 잘 동작하는 줄 알았는데, 아니었다. 이 방법도 에러가 나온다.

const options = {
  hostname: 'www.howsmyssl.com',
  port: 443,
  path: '/a/check',
  method: 'GET',
  secureProtocol: "TLSv1_2_method"
}
const req = await https.request(options)
let responseBody = '';
req.on('data', (chunk) => {
  console.log(chunk)
  responseBody += chunk;
});
req.on('end', () => {
  console.log('end')
  console.log(`BODY: ${responseBody}`);
});
req.on('error', function(e) {
  console.log('problem with request: ' + e.message);
});

Reference

  1. node.js - Configure https agent to allow only TLS1.2 for outgoing requests - Stack Overflow

[컴] NestJS 설치 및 간단한 api 생성

orm 사용 / nestjs 에서 orm 사용 / nestjs example / nestjs simple api / 사용법

NestJS 설치 및 간단한 api 생성

@nestjs/cli를 사용해서 boiler plate 를 만들 수 있다.

npm i -g @nestjs/cli

원하는 project 이름으로 만들어보자. 여기서는 myapp 으로 했다.

nest new myapp

dev server

package.json 을 참고하면 된다.

npm run start:dev

main.ts 를 보면 알겠지만, 기본 port 는 3000 으로 되어 있다. 브라우저에서 http://localhost:3000 로 접속하자. Hello World 가 보이면 된다.

config

config 파일을 사용하려면 @nestjs/config를 설치해야 한다.

npm i --save @nestjs/config

module 추가

각 기능별로 Module 을 만들어서 추가하면 된다. Application 에는 하나의 root module 이 있고, 이 root module 이 모든 module 을 import 하게 하면 된다. root module 은 nest product 를 new 할 때 만들어진다. src/app.module.ts

여기서는 MyFuncModule 을 만들어보자. Module 은 어떤 controller 를 사용하고, 어떤 provider 를 사용하는지 명시하게 된다.

controller 에서 'myfunc' 을 만들어주면 http://localhost:3000/myfunc/ 를 호출할 수 있다.

src/
  |
  + app.module.ts
  + app.controller.ts
  + app.service.ts
  |
  +-- myfunc/
        |
        + myfunc.controller.ts
        + myfunc.module.ts
        + myfunc.service.ts

AppModule :

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MyFuncModule } from './diper/myfunc.module';

@Module({
  imports: [MyFuncModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

MyFuncModule :

import { Module } from '@nestjs/common';
import { MyFuncController } from './myfunc.controller';
import { MyFuncService } from './myfunc.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MyFunEntity } from 'src/model/myfunc.entity';

@Module({
  imports: [TypeOrmModule.forFeature([MyFuncEntity]],
  controllers: [MyFuncController],
  providers: [MyFuncService],
})
export class MyFuncModule {}
import { Controller, Get } from '@nestjs/common';
import { MyFuncService } from './myfunc.service';
import { MyFuncEntity } from 'src/model/myfunc.entity';

@Controller('myfunc')
export class MyFuncController {
  constructor(private readonly myFunc: MyFuncService) {}

  @Get()
  async findAll(): Promise<MyFuncEntity[]> {
    return await this.myFunc.findAll();
  }
}
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MyFuncEntity } from 'src/model/myfunc.entity';
import { Repository } from 'typeorm';

@Injectable()
export class MyFuncService {
  constructor(
    @InjectRepository(MyFuncEntity)
    private readonly myFuncEntityRepository: Repository<MyFuncEntity>,
  ) {}

  async findAll(): Promise<MyFuncEntity[]> {
    return await this.diperRepository.find();
  }
  
  getHello(): string {
    return 'Hello World!';
  }
}
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class MyFuncEntity {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    price: number

    @Column()
    date: Date

}

Database ORM

기본으로 TypeORM 을 이용한 TypeORMModule 을 제공한다. module 은 설치해야 한다. 다음은 mysql 과 관련한 설치 command 이다. 다른 db 에 대한 정보는 TypeORM - Installation 를 참고하자.

npm install --save @nestjs/typeorm typeorm mysql2

설치를 하고 나서 TypeOrmModule 을 root module 에 import 하면 된다.

  • entites option: 아래 code 에서 entities 에 만들 table 의 정보가 들어가면 된다. 저곳에 명시하지 않으면 해당 table 을 만들지 않는다.
  • autoLoadEntities : 자동으로 entity 를 불러온다. 기본값은 false 이다.
  • synchronize : 이 값은 기본값이 true이다. 이 값을 true로 하면, entity 가 변경내역이 서버 시작 시점에 반영된다. production 에선 이 값을 false 로 두라고 한다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test', 
      entities: [],     // <---- info for tables
      synchronize: true // <---- 
    }),
  ],
})
export class AppModule {}

DataSource 를 사용하는 법

아래처럼 ConfigModule.forRoot 에서 config file 을 불러올지 정해주고, 그값들을 TypeOrmModule.forRootAsync에서 inject 해서 사용하면 된다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MyFuncModule } from './diper/myfunc.module';

import { ConfigModule, ConfigService, } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['.env.dev.local', '.env.dev'],
      isGlobal: true
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        return ({
          type: 'mysql',
          host: configService.get('HOST'),
          port: +configService.get('PORT'),
          username: configService.get('DATABASE_USER'),
          password: configService.get('DATABASE_PASSWORD'),
          database: configService.get('DATABASE'),
          // entities: [
          //   __dirname + '/**/*.entity{.ts,.js}',
          // ],
          autoLoadEntities: true,
          synchronize: true,
        })
      }
    }),
    MyFuncModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

TypeOrmModule.forRootAsync

TypeOrmModule.forRootAsync 를 사용하면, 무조건 entity 에 대한 table 을 생성하지 않는다. 각 module 에서 자신들이 사용할 entity 를 TypeOrmModule.forFeature([MyFuncEntity]) 처럼 imports에 적어주게 되는데, 이렇게 적힌 것들에 대해서만 생성해 준다.

data migration

최초의 table 의 생성은 ConfigModule.forRoot 의 option entities를 통해 정보를 가져와서 생성하게 된다.

NestJS 에서 제공하진 않고, TypeORM CLI 를 이용한다.(참고) typeorm package 가 이미 설치되어 있으니, 이걸 이용하면 된다.

migration:create :

아래처럼 하면, ./migration/test01.ts 가 생성된다. 그러면 이 test01.ts에 migration 내용을 적으면 된다.

.\node_modules\.bin\typeorm migration:create ./migration/test01

migration:run :

.ts file 을 js 로 바꿔서 사용하기 위해 ts-node 를 사용해야 한다. 다음 커맨드를 이용하면 된다.

npx typeorm-ts-node-commonjs migration:run -- -d path-to-datasource-config

full codes

Refrence

  1. First steps | NestJS - A progressive Node.js framework

[컴] NestJS 에서 ConfigModule 예시

nestjs config / env / 설정

NestJS 에서 ConfigModule 예시

NestJS 에서는 configuration 을 set할 때 ConfigService를 노출하는 ConfigModule 을 사용하는 것을 권장한다. 이 ConfigService가 알맞은 .env file 을 load 하는 것이다.

직접 만들어도 되고, @nestjs/config package 를 사용해도 된다. @nestjs/config package 내부에서 dotenv 를 사용한다.

npm i --save @nestjs/config

root module 에 아래처럼 ConfigModule을 추가한다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

import { ConfigModule,ConfigService } from '@nestjs/config';

@Module({
    // 이 때 1번째로 import 를 해야 그 이후 module 에서 ConfigModule을
    // 접근할 수 있다.
  imports: [ConfigModule.forRoot({
      // .env.dev.local 을 찾고, 없으면 .env.dev 를 찾는다.
      // 기본적으로 `.env`를 찾는다.
      envFilePath: ['.env.dev.local', '.env.dev'],
      // true 이면, global module 이 된다. 그래서 root module에서 한번만 import 하면, 
      // 다른 module 에서 import 없이 사용할 수 있다.
      isGlobal: true
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        return ({
          type: 'mysql',
          host: configService.get('HOST'),
          port: +configService.get('PORT'),
          username: configService.get('DATABASE_USER'),
          password: configService.get('DATABASE_PASSWORD'),
          database: configService.get('DATABASE'),
          // entities: [
          //   __dirname + '/**/*.entity{.ts,.js}',
          // ],
          autoLoadEntities: true,
          synchronize: true,
        })
    }}),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

.env file :

DATABASE_USER=test
DATABASE_PASSWORD=test

이제 .env file 의 변수가 load 되면, nestjs 에서 process.env.DATABASE_USER 등을 이용해서 사용할 수 있게 된다. 그리고 또한 ConfigService 에도 저장돼서 ConfigService.get() 를 통해서도 접근할 수 있게 된다. ConfigService provider는 ConfigModule.forRoot()를 통해 등록돼서 사용할 수 있게 된다.

즉 process.env 에 .env file 의 값들을 load 한 것이라 볼 수 있다.

dist directory에 복사

nest build 시에는 .ts file 외에는 자동으로 복사하지 않는다. 그래서 nest-cli.jsoncompilerOptions#assets 에 아래처럼 복사가 되도록 설정을 해놔야 한다.

{
  ...
  "compilerOptions": {
    "assets": [{"include": "../config/*.yaml", "outDir": "./dist/config"}]
  }
}

기타

yaml file, 환경변수 사용등의 자세한 이야기는 ref. 1 을 참고하자.

Reference

  1. Configuration | NestJS - A progressive Node.js framework
  2. Ultimate Guide: NestJS Config & Environment Variables [2022]

[컴][머신러닝] windows 에서 ollama 사용

 

llama / llm / 윈도우즈 /

windows 에서 ollama 사용

다음 링크에서 Ollama windows version 을 download 하자.

설치를 하면 %AppData%\Local\Programs\Ollama에 자동으로 설치된다. 대략 1.6GB 정도 여유공간이 필요하다.

  • %AppData%\Local\Programs\Ollama

아래와 같은 방식으로 실행하면 된다. model 이 없으면 model 을 download 하고, 실행한다. ollama run llama2 를 하면 llama2 model 을 다운로드하게 된다. 대략 3.8GB 이다. 이 model 은 %UserProfile%\.ollama 에 저장된다.

ollama run llama2
>>> Send a message (/? for help)
ollama list

Ollama WebUI

Ollama WebUI 가 있다. 아래는 linux 에서 설치방법이다.

sudo docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
sudo docker run -d --network=host -v ollama-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://172.17.192.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main

[컴] systemctl --user도 linger 설정이 필요하다.

스프링 이유없이 죽는 이유

systemctl --user도 linger 설정이 필요하다.

systemctl --user 를 사용하는 경우에도 linger 설정이 필요하다.

만약 ssh login 을 해서 서버를 띄운 후에 ssh세션을 모두 닫는다면 그 시점에 서버가 종료될 것이다.

sudo loginctl enable-linger <user_id>

현재 linger설정 상태를 확인하려면 아래와 같은 command를 치면 된다.

loginctl user-status <user_id>

$ loginctl user-status admin
admin (1000)
           Since: Thu 2024-03-28 06:16:00 UTC; 4h 42min ago
           State: active
        Sessions: 642 *639
          Linger: no
            Unit: user-1000.slice
                  ├─session-639.scope
                  │ ├─67433 sshd: admin [priv]
                  │ ├─67456 sshd: admin@pts/0
                  │ ├─67457 -bash
                  │ ├─67906 loginctl user-status admin
                  │ └─67907 pager
                  ├─session-642.scope
                  │ ├─67469 sshd: admin [priv]
                  │ ├─67476 sshd: admin@notty
                  │ └─67477 /usr/lib/openssh/sftp-server
                  └─user@1000.service
                    ├─app.slice
                    │ └─myserver.service
                    │   └─67481 /usr/bin/java -Dspring.profiles.active=prod -Xms512M -Xmx1G -XX:+UseZGC -jar /services/myserver/myserver-0.0.1-SNAPSHOT.jar
                    └─init.scope
                      ├─67436 /lib/systemd/systemd --user
                      └─67437 (sd-pam)

개인적인 경험

처음에 systemctl --user 를 사용할 떄는 이것이 systemctl 과 같다고 봐서, daemon 으로 동작하기에 따로 nohup 등의 설정이 필요없다고 생각했다. 그래서 systemctl --user를 이용해서 웹서버를 실행했었다.

하지만 이 서버가 종종 gracefully shutdown 으로 죽었다.(spring server 였다.) 당연히 웹서버(WAS) 로그에는 남아있는 것이 없었다.

원인을 찾다가 deamon.log를 보게 되었다. 여기서 systemd가 stop 된 것을 보고 웹서버(WAS)가 죽은 이유를 추측해 볼 수 있었다. 그래서 결국 linger 설정이 필요한 것을 알았다.

cat /var/log/daemon.log | grep "Mar 28 06:12"

...
Mar 28 06:12:45 i-0127a77bf7ac67eaf systemd[1]: Stopping User Manager for UID 1000...
Mar 28 06:12:45 i-0127a77bf7ac67eaf systemd[104663]: Stopped target Main User Target.
Mar 28 06:12:45 i-0127a77bf7ac67eaf systemd[104663]: Stopping PMS api server(fish)...

Reference

  1. https://github.com/systemd/systemd/issues/8486#issuecomment-374318982

[컴] jenkins 의 plugin list 를 추출하는 법

jenkins plugin 설치 / batch / cli 에서 설치 / 뽑기

jenkins 의 plugin list 를 추출하는 법

jenkins web page 에서 추출하기

https:///script 로 가서

Jenkins.instance.pluginManager.plugins.each{
  plugin -> 
    println ("${plugin.getShortName()}:${plugin.getVersion()}")
}

jenkins cli 로 뽑기

cli 는 다음처럼 받을 수 있다.

curl https:///jnlpJars/jenkins-cli.jar -o jenkins-cli.jar
// get_plugin_list.groovy
def plugins = jenkins.model.Jenkins.instance.getPluginManager().getPlugins()
plugins.each {println "${it.getShortName()}: ${it.getVersion()}"}
java -jar jenkins-cli.jar -s https:// -auth "namh:who1sthegoodman" groovy = < get_plugin_list.groovy >  plugins.txt

jenkins cli 로 plugin 설치

testing - How to install jenkins plugins from command line? - Stack Overflow

java -jar jenkins-cli.jar -s https://inhouse.foodpang.co/ci -auth <username>:<password> install-plugin <plugin_name1> <plugin_name2> ...

Reference

  1. How to get a list of installed Jenkins plugins with name and version pair - Stack Overflow

[컴][유틸] 2024-03, 에디터

hexa editor / hex editor / 괜찮은 hex editor

2024-03, 에디터

See Also

  1. 쿠…sal: [컴] sublime text 2 에서 binary 파일을 text 파일로 열기
  2. 쿠…sal: [컴][유틸] 대용량 파일 뷰어 big size file viewer

[컴] gradle java build 에서 git commit hash id 를 추가

스프링 / 스프링부트 / spring /

gradle java build 에서 git commit hash id 를 추가

com.gorylenko.gradle-git-properties gradle plugin 을 이용할 것이다.

build.gralde

아래처럼 build.gradle 을 설정하자. build 를 하면 build/resources/main/git.properties 가 만들어진다.

plugins {
  id "com.gorylenko.gradle-git-properties" version "2.4.1"
}
...
gitProperties {
    keys = ['git.branch','git.commit.id','git.commit.time']
    customProperty 'greeting', 'Hello' // expression
    // Customize file name (could be a file name or a relative file path below gitPropertiesResourceDir dir)
    gitPropertiesName = "my-git-file.properties"
    dotGitDirectory = file("${project.rootDir}/.git") # 정의하지 않으면, 자동으로 .git folder를 찾는다.
}

git.properties

위에서 gitPropertiesName 로 properties file 의 이름을 변경했기에, build/resources/main/my-git-file.properties 가 만들어진다.

my-git-file.properties 의 내용은 다음과 같다.

// build/resources/main/my-git-file.properties
git.branch=mybranch_name
git.commit.id=0c5da9f4f7312bb6c02f21c47ae129b31a006046
git.commit.time=2024-03-10T11\:18\:37+0900
greeting=Hello

jar 에서 위치

그리고 이것은 gradlew build 를 할 때 아래 경로에 들어가게 된다.

  • myjar-0.0.1.jar\BOOT-INF\classes\git.properties

SpringBoot

[컴] amazone 의 Correctto 17

 

amazone 의 Correctto 17

아마존에서 제공하는 OpenJDK 17 로 보면 된다. LTS(Long Term Support) 를 지원한다.

curl -LO https://corretto.aws/downloads/latest/amazon-corretto-17-x64-linux-jdk.tar.gz

아래 링크에 가면, deb, rpm 등도 다운로드 받을 수 있다.

[컴] MySql virtual column

mariadb 마리아db /

MySql virtual column

ALTER TABLE t1 ADD COLUMN c2 INT GENERATED ALWAYS AS (c1 + 1) STORED;
ALTER TABLE t1 ADD COLUMN c2 INT GENERATED ALWAYS AS (c1 + 1) VIRTUAL;

다른 table의 column을 이용하려면, view를 만드는 것도 방법이다.

Reference

  1. Generated (Virtual and Persistent/Stored) Columns - MariaDB Knowledge Base
  2. mysql - Stored/Virtual Generated Column- Pros/Cons/Best Practices? - Stack Overflow