Swagger
Swagger는 API 명세들을 시각화 해주는 프레임워크이다.
더 많은 설명은 아래 공식 문서에서 확인할 수 있다.
API Documentation & Design Tools for Teams | Swagger
Loved by all • Big & Small Thousands of teams worldwide trust Swagger to deliver better products, faster.
swagger.io
현재 Spring Rest Docs를 이용하여 테스트 기반으로 YAML 형식의 API 명세를 뽑아내고 있는 상황이다.
하지만 프론트엔드 팀원에게 YAML 파일을 그대로 던져주면 보기에도 불편하고 계속 파일을 주고 받아야 한다.
따라서 우리는 효율적인 협업을 위해 API 명세를 간결하게 시각화 해주는 Swagger를 도입하기로 결정했다.
프로젝트 아키텍쳐
현재 3개의 스프링 서비스를 두고 있으며, 각각의 빌드 과정마다 테스트가 수행된다.
테스트를 기반으로 YAML 형식의 API 명세가 뽑혀져 나오는데, 이 YAML 파일들을 보여줄 Swagger 서버 구축이 목표다.
즉, 완성해야 하는 아키텍쳐는 다음과 같다.
EKS 환경에 Swagger 배포
Swagger 이미지 확인
Swagger 이미지는 Docker hub에서 확인할 수 있다.(딱히 관리가 잘 되고 있는 것 같진 않다)
나는 이 이미지를 베이스로 EKS상에 배포하려 했다.
https://hub.docker.com/r/swaggerapi/swagger-ui
Docker
hub.docker.com
아래는 배포에 필요한 리소스를 정의한 파일이다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: swagger-ui
labels:
app: swagger-ui
spec:
replicas: 1
selector:
matchLabels:
app: swagger-ui
template:
metadata:
labels:
app: swagger-ui
spec:
containers:
- name: swagger-ui
image: swaggerapi/swagger-ui
ports:
- containerPort: 8080
env:
- name: BASE_URL
value: /
- name: API_URLS
value: >-
[
{url:'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml',name:'Pet Store Example'},
{url:'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/uspto.yaml',name:'USPTO'}
]
---
apiVersion: v1
kind: Service
metadata:
name: swagger-ui
spec:
selector:
app: swagger-ui
ports:
- name: http
port: 80
targetPort: 8080
type: ClusterIP
그렇게 Pod를 생성하려는 순간
spec.env를 보면 Swagger가 관리할 YAML을 로컬 볼륨이 아닌 외부 url로도 매핑해 줄 수 있는것을 발견했었다.
그때 든 생각은, 괜히 지저분하게 로컬로 관리하지말고 S3 같은 외부 스토리지를 사용하자 였다!
각 서비스들은 현재 Jenkins에서 Build되고 있으므로, Jenkins에서 S3에 PutObject를 해주기만 한다면
Swagger는 GetObject로 가져오기만 하면 될거라 생각했다.
더군다나 환경변수는 런타임에 바꾸기 쉽지 않아서 이 방식이 더 적합하다고 생각이 들었다.
아키텍쳐는 다음과 같이 바뀌게 되었다.
다시 돌아와서 S3와 연동하기 이전에, 위에서 정의한 리소스가 제대로 동작하는지 부터 확인했다.
Swagger 동작 확인
예제 YAML파일로 구성된 Swagger를 EKS 상에 배포했다.
현재 클러스터 네트워킹은 Ingress를 통해 각 Service로 라우팅 되는 시스템이었다.
따라서 Ingress에 새로운 Rule을 추가해주었다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: quiz-it-ing
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
ingressClassName: alb
rules:
- http:
paths:
...
- path: /api/user
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
- path: /api/swagger
pathType: Prefix
backend:
service:
name: swagger-ui
port:
number: 80
...
기존 네이밍룰을 따라 /api/swagger/*로 오는 트래픽은 swagger-ui 서비스가 담당하도록 설정했다.
따라서 http://[ALB 도메인]/api/swagger/ 로 접근하면 Swagger 그리팅 페이지가 맞이하길 기대했다.
동작이 안되고 있어서 Pod 내부로 직접 들어가서 curl 요청을 해보았다.
내부에서는 정상 동작, 외부에서는 404가 뜨고 있는 상황이었다.
무엇이 문제일까 고민하다가 404 페이지를 다시 보니 nginx까지는 도달한 것을 알 수 있었다.
내친김에 Pod의 로그를 확인해보니
모자이크 크기조절이 잘 안되어 부득이하게 라이언으로 대체했다.
아무튼 로그를 확인해보면 GET /api/swagger/ 요청이 /usr/share/nginx/html/api/swagger 리소스가 없어서 Fail 되었다.
즉, 우리가 위에서 제대로 동작을 확인한 요청은 GET /api/swagger/ 가 아닌 GET / 였다는걸 알 수 있다.
Ingress Rewrite Target
가장 먼저 든 생각은 Ingress 단에서 요청 주소를 바꿔주는걸 생각했다.
기존의 네이밍 룰(/api/*)를 따라야 했기에 /api/swagger/로 들어오는 요청을 /으로 바꿔주는 것이다.
하지만, 찾아본 결과 nginx와는 달리 alb-ingress-controller는 공식적으로 이 기능을 지원하지 않았다.
BASE_URL 수정
다시 고민을 하던중 Swagger Pod를 정의한 YAML파일을 다시 보니 Base Url이 /으로 설정된것이 보였다.
...
env:
- name: BASE_URL
value: /
- name: API_URLS
value: >-
[
{url:'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml',name:'Pet Store Example'},
{url:'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v3.0/uspto.yaml',name:'USPTO'}
]
...
그렇다면 BASE_URL을 /api/swagger/ 로 설정하면 되지 않을까?
이게 된다면 기존의 Ingress 구조를 그대로 가져갈 수 있게 된다!
BASE_URL을 수정한 다음, 다시 Pod를 생성해보았다.
맨 끝 주소에 / 을 붙여야 함에 유의하자.
드디어 예제 YAML로 구성된 Swagger 페이지가 보인다.
동작을 확인 했으니 이번엔 S3와의 연동으로 넘어갔다.
S3 연동
S3와의 연동을 위해서는 우선 S3 버킷이 필요했다.
Terraform을 통해 S3 버킷을 만드려는 순간, 여러가지 생각이 머릿속을 스쳐지나갔다.
S3에 파일을 올리고 다운받는 권한 설정은 어떻게 할 것인가?
S3에 파일을 올리고 다운받는 권한은 누구에게 부여 할 것인가?
S3에 파일을 올리고 다운받는 권한은 어떻게 관리 할 것인가?
처음엔 복잡했지만 이전에 IAM Role과 Service Account에 대해 정리해둔 내용을 바탕으로 천천히 생각을 다시 해보았다.
https://imsongkk.tistory.com/73
[AWS] IAM과 Role, Policy
IAM 항상 나는 AWS에 로그인할 때 루트 유저로 로그인했었다. 책이나 다른 실습 강의에서는 항상 루트 사용자로 로그인했었기에, 그냥 그런가보다 하고 넘겼었다. 하지만 AWS에 점점 익숙해지면서,
imsongkk.tistory.com
S3에 파일을 올리고 다운받는 권한 설정은 어떻게 할 것인가?
=> Terraform을 통해 적절한 권한을 갖는 Policy를 만든다.
S3에 파일을 올리고 다운받는 권한은 누구에게 부여 할 것인가?
=> Terraform을 통해 Swagger전용 SA를 만들고 해당 SA에 Role을 부여한다.
S3에 파일을 올리고 다운받는 권한은 어떻게 관리 할 것인가?
=> Terraform을 통해 Role을 만들고, 기존에 만들었던 Policy를 부여한다.
해당 내용이 반영된 Terraform 코드를 살펴보자.
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "quizit-swagger"
acl = "private"
tags = var.tags
}
resource "aws_iam_policy" "get_s3" {
name = "get_s3_policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = "*"
},
]
})
}
module "irsa_role" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
role_name = "Swagger_S3"
allow_self_assume_role = false
oidc_providers = {
ex = {
provider_arn = var.oidc_provider_arn
namespace_service_accounts = ["default:swagger"]
}
}
role_policy_arns = {
Get_S3_Policy = aws_iam_policy.get_s3.arn
}
tags = var.tags
}
resource "kubernetes_service_account" "swagger" {
metadata {
name = "swagger"
namespace = "default"
annotations = {
"eks.amazonaws.com/role-arn" = module.irsa_role.iam_role_arn
}
}
}
특이한건 기존의 RBAC 기반이 아닌 Annotation 기반으로 Role을 설정했다는 점이다.
이는 아마 AWS내에서 자격증명을 하는 방식이 Annotation 기반이어서 그런 것 같다..
아무튼 이렇게 만들고, Swagger Pod 리소스에 ServiceAccount를 입혀주었다.
apiVersion: apps/v1
kind: Deployment
...
spec:
serviceAccountName: swagger
containers:
- name: aws-cli
image: amazon/aws-cli:latest
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
- name: swagger-ui
image: swaggerapi/swagger-ui
ports:
- containerPort: 8080
...
swagger라는 ServiceAccount를 입혀주었고, aws-cli 컨테이너를 추가해 정말 s3 오브젝트에 접근이 되는지 확인을 했다.
각 서비스들의 API 명세를 제대로 가져오는것을 보니 성공이다.
그렇다면 이제 Swagger YAML 파일의 주소만 바꾸어 주면 끝이다!
apiVersion: apps/v1
...
env:
- name: BASE_URL
value: /
- name: API_URLS
value: >-
[
{url:'https://quizit-swagger.s3.ap-northeast-2.amazonaws.com/auth.yml', name:'AUTH'},
{url:'https://quizit-swagger.s3.ap-northeast-2.amazonaws.com/quiz.yml', name:'QUIZ'},
{url:'https://quizit-swagger.s3.ap-northeast-2.amazonaws.com/user.yml', name:'USER'}
]
...
다시 apply한 이후 Swagger를 확인해보았다.
CORS 에러
CORS 에러가 발생하는것을 볼 수 있다.
바로 S3 Bucket에 CORS 정책을 넣어주었다.
module "s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
bucket = "quizit-swagger"
acl = "private"
cors_rule = [
{
allowed_headers = ["*"]
allowed_methods = ["GET", "POST"]
allowed_origins = ["*"]
max_age_seconds = 3000
}
]
control_object_ownership = true
object_ownership = "ObjectWriter"
tags = var.tags
}
추후 고정 도메인이 생긴다면 allowed_origins에 *이 아닌 해당 도메인만 추가할 예정이다.
403 Forbidden
여기서 잠깐 당황했었다. 분명 aws cli로 테스트 했을 때는 SA의 Role을 가져와서 자격증명이 올바르게 됐었다.
왜그럴까를 한참을 고민하다가 너무 단순하게 생각했던 나를 발견했다.
Pod를 실행할 때, Service Account를 명시할 수 있다.
해당 Service Account의 IRSA은, Pod가 생성될 때 ARN 형태로 환경변수로 주입이 된다.
aws cli는 컨테이너단에서 바로 실행되므로, 내부적으로 환경 변수를 읽어와 올바른 자격증명을 가질 수 있지만
Swagger는 AWS SDK나 CLI를 거치지 않기에 aws와 통합하려면 Token 기반의 http 통신이 필요한 상황이었다.
우선 천천히 해결할 방법을 생각해보았다.
- Swagger GET 요청 코드를 직접 수정한다.
=> 너무 무모하고 어떤 장애를 일으킬지 모른다. - Swagger Pod에 Sidecar 컨테이너를 두어 Pod를 시작할 때 S3로부터 YAML을 가져와 emptyDir에 저장한다
=> 하지만 실시간 리로드가 안된다. - Swagger Pod에 AWS SDK가 설치된 proxy 서버를 따로 두어 요청을 처리한다
=> 가장 합리적인 생각이었다.
MSA의 장점을 살리기 위해 proxy 서버는 비교적 가벼운 Flask를 채택했다.
그렇게 열심히 설정을 하던중에 Swagger가 OAS를 가져오는 코드는
서버가 아닌, 브라우저 단에서 호출한다는것을 깨달았다.
즉, proxy 서버를 거칠 틈도 없이 바로 브라우저에서 호출을 해버리니 의미가 없었다.
다시 고민끝에 결국은 S3 버킷을 퍼블릭으로 설정하기로 결정했다.
퍼블릭으로 결정한 이유는 다음과 같다.
1. 프라이빗이어도 Swagger 서버의 URL이 노출되면 어차피 OAS 파일이 다 보이게 된다.
=> 이를 막으려면 별도의 로그인창이 구현된 커스텀 WAS를 구축해야 한다.
2. 퍼블릭이어도 세부 권한을 나눌 수 있다.
=> 모든이들이 접근할 수 있지만, GetObject 권한만 기본적으로 설정하면 URL이 노출되어도 read-only 작업만 할 수 있다.
S3 버킷에 적재할 수 있는 권한은 Jenkins CI에만 적용하면 된다!
간단한 방법으로 우회
우선, S3 Terraform 코드를 다음과 같이 수정했다.
resource "aws_s3_bucket" "swagger" {
bucket = "quizit-swagger"
}
resource "aws_s3_bucket_cors_configuration" "swagger" {
bucket = aws_s3_bucket.swagger.id
cors_rule {
allowed_headers = [""]
allowed_methods = ["GET"]
allowed_origins = ["*"]
}
}
resource "aws_s3_bucket_public_access_block" "swagger" {
bucket = aws_s3_bucket.swagger.id
block_public_acls = true
block_public_policy = false
ignore_public_acls = true
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "swagger" {
bucket = aws_s3_bucket.swagger.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = "*",
Action = "s3:GetObject",
Resource = "${aws_s3_bucket.swagger.arn}/*"
}
]
})
}
이렇게 설정하면 어느 누구도 URL만을 가지고 버킷에 read-only 권한으로 접근할 수 있다.
그런 다음, Jenkins 에서 빌드를 끝내고 버킷에 OAS yaml 파일을 올릴 수 있는 권한을 주었다.
module "jenkins_irsa_role" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
role_name = "jenkins_role"
oidc_providers = {
ex = {
provider_arn = var.oidc_provider_arn
namespace_service_accounts = ["jenkins:jenkins"]
}
}
role_policy_arns = {
policy = module.iam_policy.arn
ecr_policy_managed = "arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilderECRContainerBuilds"
}
}
module "iam_policy" {
source = "terraform-aws-modules/iam/aws//modules/iam-policy"
name = "jenkins_policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
]
Resource = "${var.swagger_bucket_arn}/*"
}
]
})
}
이제 Jenkins pipeline에서는, jenkins sa에 입혀진 자격증명에 있는 policy를 이용해서
aws cli를 통해 자유롭게 S3 버킷에 접근할 수 있게 되었다.
다시 Swagger 동작 확인
S3를 퍼블릭으로 설정하니 바로 Swagger 브라우저에서 OAS yaml 파일을 가져올 수 있었다.
아쉬운 점
외부 사용자가 수정을 하지 못하긴 해도, 볼 수 있다는건 여전히 찝찝하다.
보안을 더 강화해서 아예 document 들을 모아놓은 서버를 구축하고 싶었으나, 다른 개발할것들이 많이 남아서 아쉽다.
'Trouble Shooting' 카테고리의 다른 글
Jenkins Webhook Multiple branch 빌드 하기 (0) | 2023.08.28 |
---|---|
ALB Ingress Controller로 여러 개의 Ingress 처리하기 (0) | 2023.08.11 |
AWS EKS Jenkins 환경 구축하기 [2/2] (0) | 2023.07.31 |
AWS EKS Jenkins 환경 구축하기 [1/2] (0) | 2023.07.31 |
AWS EKS 클러스터 구성 및 배포하기 [2/2] (1) | 2023.07.02 |