주저리
microservice!
microservice는 작디작은 서비스다.
그리고 Microservice Architecture(MSA)는 microservice 집합으로 어플리케이션을 이룬다.
From Monolith application to Microservice application
한편 현존하는 과반수의 앱의 구조는 MSA와는 거리가 멀 것이다.
(우리 회사 포함)
대대적인 개편의 바람이 불어올 때 그제서야 MSA를 차용할 거란 말이다.
한편 MSA 적용은 마치 18세기 유럽의 산업 혁명의 모습과 비슷하다.
아래와 같은 점에서 말이다.
- 새로운 도구의 활용 (Docker, Kubernetes, Istio, Argo 등)
- 새로운 분업 체계 (bounded context)
따라서 MSA의 적용은 단순히 서비스의 개수가 여러 개로 늘어나는 것뿐만 아니다.
환골탈태 수준의 부가적인 변화를 동반한다.
한편 Microservice 전환 시 고려사항 중 중요한 것은
'cross-cutting concern'이다.
복수의 서비스에 공통적으로 필요한 것 말이다.
인증/인가는 그 대표적인 예시다.
end user의 request 검증은 어느 서비스나 필요할 테니까 말이다.
여하튼 서론은 슬슬 마치고 본론으로 들어간다.
이 글의 제목과 주제를 밝힌다.
제목은 Microservice 인증/인가로 하겠습니다.
근데 이제 Istio를 곁들인.
분석 및 설계
Microservice는 인증이 필요해
서비스는 요청 정보를 반드시 검증해야 한다.
검증 과정이 없다면 외부 이용자에 의해 시스템 리소스가 엉망진창이 될 위험이 있기 때문이다.
위 예시에서는 인증/인가(authn/z) 역할을 각각의 서비스에 구현했다.
이를 통해 외부 요청으로부터 시스템과 리소스를 안전하게 지킬 수 있다.
하지만 불편함이 눈에 띈다.
'중복'이라는 불편함이 말이다.
Istio 인증/인가
인증/인가를 별도의 서비스로 분리하자!
Istio를 이용해서 다음과 같은 구조를 차용할 수 있다.
이전보다 두 가지 컴포넌트가 추가됐다.
하나씩 그 역할을 알아보자.
Ingress Gateway
모든 외부 요청은 Istio Ingress Gateway를 거친다.
외부 요청 정보를 분석한다.
별도의 인증이 필요할 경우 인증 서비스에 요청을 전달한다.
예컨대 다음과 같은 두 가지 요청에 대해 생각해보자.
GET /user/123
Host: www.example.com
특정 사용자의 개인정보 조회는 회원 인증이 필요할 수 있다.
또한 회원은 자신의 정보만 조회할 수 있다면, 권한 확인(인가) 또한 필요하다.
Ingress Gateway는 해당 요청을 authN/Z 서비스로 전달한다.
GET /catalog/81
Host: www.example.com
상품 정보는 누구나 조회할 수 있다. 인증 처리가 불필요하다.
외부 요청을 곧바로 catalog 서비스로 전달한다.
authN/Z
요청에 대한 인증 및 인가 처리를 담당한다.
로그인 또는 사용자 권한 등을 확인하는 것이 그 예다.
구현
상세 코드는 아래 Github에서 확인 가능
Istio
인증 서비스
환경 설정
구현에 앞서 아래와 같은 환경 설정이 선행되어야 한다.
- kubernetes 클러스터
- kubectl
- Istio 설치
- istioctl
- 테스트용 모의 서비스
- htttpbin
- sleep
테스트용 모의 서비스는 다음과 같이 배포한다.
sample 파일은 istioctl에 포함된 것을 참고한다.
kubectl create ns foo
kubectl label ns foo istio-injection=enabled
kubectl apply -f samples/httpbin/httpbin.yaml -n foo
kubectl apply -f samples/sleep/sleep.yaml -n foo
서비스 공개
현재까지의 설정으로는 Kubernetes 클러스터 내부 통신만 가능하다.
몇 가지 설정을 추가해서 외부에 서비스를 공개해보자.
Gateway
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: httpbin-gateway
spec:
selector:
istio: ingressgateway # use Istio default gateway implementation
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
kubectl apply -f https://github.com/kakaru1331/istio-sandbox/blob/main/task/external-authorization/custom/gateway.yaml -n foo
클러스터 외부 요청을 받기 위해 Gateway를 생성한다.
80 port 대상의 요청을 처리할 수 있다.
VirtualSerivce
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: httpbin
spec:
hosts:
- "*"
gateways:
- httpbin-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
port:
number: 8000
host: httpbin
k apply -f https://raw.githubusercontent.com/kakaru1331/istio-sandbox/main/task/external-authorization/custom/virtualservice.yaml -n foo
Virtual Service는 특정 Gateway로 들어온 요청을 골라서 처리할 수 있다.
httpbin-gateway로 들어온 모든 요청을 httpbin 서비스로 전달한다.
여기까지 설정을 마쳤다면 위와 같이 httpbin 페이지를 확인할 수 있을 것이다.
중간 정리
Gateway, VirtualService, httpbin 등을 등록했다.
덕분에 클러스터 외부의 요청을 접수하여 downstream 서비스로 전달할 수 있다.
위 diagram과 같은 구조로 통신이 이루어진다.
인증/인가 서비스
인증/인가 서비스를 추가한다.
해당 서비스의 역할은 사용자 요청 header 검증을 하는 것이다.
요청에 따라서 다음과 같이 세 가지 유형의 응답을 반환한다.
- 인증 header 없음 -> 인증 실패
HTTP Status Code: 301 (redirect)
google 로그인 페이지로 redirect 처리 - 인증 값 유효하지 않음 -> 인증 실패
HTTP Status Code: 403 (unauthorized) - 인증 성공
HTTP Status Code: 200 (OK)
인증 성공 조건은 아래와 같다.
Request Headers:
x-ext-authz: allow
인증 서비스는 Java로 작성했다.
package me.kakaru.xauth;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
@RestController
public class WebController {
String AUTH_HEADER = "x-ext-authz";
String ALLOWED_VALUE = "allow";
String DENY_BODY = String.format("denied by ext_authz for not found header %s: %s", AUTH_HEADER, ALLOWED_VALUE);
String OAUTH_URI = "https://accounts.google.com/signin";
@RequestMapping(value = "**", method = { RequestMethod.GET, RequestMethod.POST})
public ResponseEntity index(HttpServletRequest request) {
String authHeader = request.getHeader(AUTH_HEADER);
if (StringUtils.isEmpty(authHeader)) {
return ResponseEntity
.status(HttpStatus.MOVED_PERMANENTLY)
.location(URI.create(OAUTH_URI))
.build();
} else if (checkAuthHeader(authHeader)) {
return ResponseEntity.status(HttpStatus.OK).build();
} else {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(DENY_BODY);
}
}
boolean checkAuthHeader(String authHeader) {
if (StringUtils.isEmpty(authHeader)) {
return false;
} else if (!ALLOWED_VALUE.equals(authHeader)) {
return false;
}
return true;
}
}
해당 서비스의 container image는 Github registry에 등록해두었다.
이를 활용하여 인증 서비스를 클러스터에 배포한다.
apiVersion: v1
kind: Service
metadata:
name: custom-authz
labels:
app: custom-authz
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: custom-authz
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-authz
spec:
replicas: 1
selector:
matchLabels:
app: custom-authz
template:
metadata:
labels:
app: custom-authz
spec:
containers:
- image: ghcr.io/kakaru1331/xauth:latest
imagePullPolicy: IfNotPresent
name: custom-authz
ports:
- containerPort: 8080
kubectl apply -f https://raw.githubusercontent.com/kakaru1331/istio-sandbox/main/task/external-authorization/custom/ext-authz-custom.yaml -n foo
배포 후 아래 명령어를 사용해서 인증 서비스를 검증한다.
인증 header key & value가 없기 때문에 301 Status Code를 반환한다.
kubectl exec "$(kubectl get pod -l app=sleep -n foo -o jsonpath='{.items..metadata.name}')" -c sleep -n foo -- curl http://custom-authz.foo:8080/ -s -o /dev/null -w "%{http_code}\n"
301
Extension Provider
인증 서비스를 Extension Provider로 등록한다.
추후 Authorization Policy에서 해당 provider를 사용할 것이다.
data:
mesh: |-
# extensionProviders 추가
extensionProviders:
- name: "custom-authz-http"
envoyExtAuthzHttp:
service: "custom-authz.foo.svc.cluster.local"
port: "8080"
includeRequestHeadersInCheck: ["x-ext-authz"]
accessLogFile: /dev/stdout
defaultConfig:
discoveryAddress: istiod.istio-system.svc:15012
proxyMetadata: {}
tracing:
zipkin:
address: localhost:9411
enablePrometheusMerge: true
enableTracing: false
outboundTrafficPolicy:
mode: ALLOW_ANY
trustDomain: cluster.local
meshNetworks: 'networks: {}'
Istio Authorization Policy
'AuthorizationPolicy'는 특정 서비스로의 요청에 인증 처리를 덧붙일 수 있다.
httpbin workload로 향하는 요청에 인증 정책을 명시한다.
그리고 모든 요청이 아니라, '/headers' path에 해당하는 요청만 인증 처리한다.
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: custom-authz
spec:
selector:
matchLabels:
app: httpbin
action: CUSTOM
provider:
# The provider name must match the extension provider defined in the mesh config.
# You can also replace this with sample-ext-authz-http to test the other external authorizer definition.
name: custom-authz-http
rules:
# The rules specify when to trigger the external authorizer.
- to:
- operation:
paths: ["/headers"]
kubectl apply -f https://raw.githubusercontent.com/kakaru1331/istio-sandbox/main/task/external-authorization/custom/authorization-policy.yaml -n foo
리뷰
마침내 서두에서 언급한 대로 구현해냈다.
Microservice 인증/인가
근데 이제 Istio를 곁들인.
마지막으로 인증 서비스 검증을 해보자.
1. 인증 header 없음 -> 인증 실패
HTTP Status Code: 301 (redirect)
google 로그인 페이지로 redirect 처리
2. 인증 값 유효하지 않음 -> 인증 실패
HTTP Status Code: 403 (unauthorized)
kubectl exec "$(kubectl get pod -l app=sleep -n foo -o jsonpath='{.items..metadata.name}')" -c sleep -n foo -- curl http://httpbin.foo:8000/headers -H
"x-ext-authz: deny" -s
denied by ext_authz for not found header x-ext-authz: allow
3. 인증 성공
HTTP Status Code: 200 (OK)
kubectl exec "$(kubectl get pod -l app=sleep -n foo -o jsonpath='{.items..metadata.name}')" -c sleep -n foo -- curl http://httpbin.foo:8000/headers -H
"x-ext-authz: allow" -s
{
"headers": {
"Accept": "*/*",
"Host": "httpbin.foo:8000",
"User-Agent": "curl/7.81.0-DEV",
"X-B3-Parentspanid": "884299afdb8f44e4",
"X-B3-Sampled": "0",
"X-B3-Spanid": "48b39b00dfe50674",
"X-B3-Traceid": "715931340af86da8884299afdb8f44e4",
"X-Envoy-Attempt-Count": "1",
"X-Ext-Authz": "allow",
"X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/foo/sa/httpbin;Hash=421dc812743a90f4bafae747ead1228b8f44383dcb0dbd98f09e6d8977a3c4cc;Subject=\"\";URI=spiffe://cluster.local/ns/foo/sa/sleep"
}
}
마치며
먹고 살기 쉽지 않음. (ㅠ.ㅠ)
배우랴, 만들랴, 글쓰랴.
여친도 없는데 인생 왜 이리 바쁜지...
여하튼 훗날 이 글을 읽을 독자에게 도움이 되길 바라며. (^3^)
References
'공학 > 정보성' 카테고리의 다른 글
Windows kubectl alias 설정 (0) | 2022.02.27 |
---|---|
코딩 컨벤션: 예시로 알아보자! (0) | 2021.09.05 |
node 프로젝트 라이브러리 버전 관리법 (0) | 2021.01.24 |
자바스크립트 클로저(closure) (0) | 2020.08.19 |
오라클 끄기 / 켜기 (0) | 2020.08.16 |