Terraform + AWS로 웹 서비스 인프라 구축하기 (3)

동관·2024년 12월 19일
post-thumbnail

시작하며

저번 글에서는 EC2, EKS, RDS까지 만들어보았으니 이번엔 웹, 애플리케이션을 배포해볼 차례이다.

아래 링크에서 RDS DB와 연결돼 EKS에 배포될 Spring Boot Sample APP과 CloudFront + S3로 정적 웹 호스팅할 VueJS Sample Project를 받았다.

kubectl, eksctl, helm

저번 시간에 생성한 EC2 배스천 호스트에 kubectl, eksctl, helm 명령어를 받아야 한다.

kubectl은 Kubernetes API와 통신 가능한 명령어 도구이고, eksctl은 EKS 생성, 관리에 관한 도구, helm은 쿠버네티스 네이티브 패키지 매니저라고 생각하면 된다.

아래 링크를 통해 모두 설치해주자.

kubectl 명령어를 설치했다면 아래의 명령어로 EKS Kubernetes API와의 통신 정보를 Kubeconfig 파일로 가져오자.

aws eks update-kubeconfig --region ap-northeast-2 --name eks-cluster-ap-northeast-2-app

해당 config 파일은 홈 디렉토리의 .kube/config로 저장된다.

config 파일을 저장했으면 kubectl get po -A을 입력해 정상적으로 통신이 되는지를 확인해보자.

ArgoCD

EKS에 애플리케이션을 배포할 방법으로 ArgoCD을 사용해보자.

ArgoCD란 Gitops 패턴을 구현한 Continuous Deployment 도구이며, 쉽게 생각하여 깃으로 쿠버네티스에 배포되는 모든 리소스, 오브젝트를 관리한다고 생각하면 된다.

ArgoCD는 CNCF Graduated Project로 사용성과 안정성을 검증받아 쿠버네티스를 사용하는 많은 회사에서 사용하고 있는 오픈 소스 솔루션이다.

공식 문서에 나와있는 대로 배포하자.

kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

배포가 다 되었다면 kubectl get po -n argocd을 입력해 정상적으로 파드가 실행되고 있는지 확인하자.

정상적으로 실행되는 것을 확인했다면 이제 ArgoCD 웹으로 접속할텐데 그러기 위해서는 몇 가지 단계를 거쳐야 한다.

  1. Service Type : LoadBalancer? Ingress?
  • 서비스 유형을 로드 밸런서로 생성해 외부에 오픈할지, 인그레스를 사용할 지를 정해야 하는데 쉽게 로드밸런서는 L4, 인그레스는 L7이라고 생각하면 된다.
    ArgoCD 앱에서 TLS 설정을 해주겠다면 로드 밸런서로, 앱에서 안하고 인그레스에서 TLS 설정을 하겠다면 인그레스를 선택하면 된다.
    둘다 가능하다면, 인그레스를 추천한다. 인그레스가 호스트, 라우팅 관련된 설정이 더 많기 때문이다.
  1. Application LoadBalancer Controller
  • 인그레스를 사용하기로 정했다면 인그레스 클래스로 AWS의 ALB를 이용할 것인데, 인그레스를 생성할 때 동적으로 ALB를 생성해 연결시키려면 Application LoadBalancer Controller를 설치해야 한다.

아래 문서를 참고해 설치하자.

설치가 완료되었다면 kubectl get po -n kube-system 명령어로 ALC 파드가 정상인지 확인해보자

[dgyoon@ip-10-10-1-141 ~]$ kubectl get po -n kube-system
NAME                                            READY   STATUS    RESTARTS   AGE
aws-load-balancer-controller-7f784cc58d-4frjd   1/1     Running   0          6m10s
aws-load-balancer-controller-7f784cc58d-6k88w   1/1     Running   0          6m10s

이제 ingress.yaml을 작성하자

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server-ingress
  namespace: argocd
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
    alb.ingress.kubernetes.io/target-type: instance
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: argocd-server
            port:
              number: 80

상세한 명세서는 공식 문서에 전부 나와있고 이 문서만 설명하자면 일단 어노테이션으로 해당 인그레스가 사용할 ALB의 요구 조건을 작성한다.

우리는 인터넷 웹으로 접근할 것이기 때문에 alb.ingress.kubernetes.io/scheme: internet-facing를 추가하고, 헬스체크, 포트는 80 포트로 오픈한다.

alb.ingress.kubernetes.io/target-type는 인스턴스 유형과 파드 IP 유형 두 가지의 선택지가 있는데 파드 IP는 주로 Fargate에서 사용한다고 하므로 우리는 instance로 선택한다.

* 인스턴스 유형의 경우 공개하려는 서비스를 ClusterIP가 아닌 NodePort로 오픈해주어야 한다. *

아래 ingressClassName에는 alb를 넣어주고 하위 스펙들은 기본적으로 /* 경로로 들어온 트래픽을 어느 서비스로 넘겨줄 지 정한다.

ACM과 Route53으로 도메인 SSL 인증서를 발급받은 뒤 어노테이션 alb.ingress.kubernetes.io/certificate-arn에 인증서 ARN을 채워주면 https로 접근 가능하나 이번 실습에서는 생략했다.

ArgoCD 앱에서 TLS 설정을 안하기로 한 경우 관련 컨피그맵을 수정해주어야 한다.

kubectl patch cm -n argocd argocd-cmd-params-cm --type merge -p '{"data":{"server.insecure":"true"}}'

그리고 kubectl rollout restart deploy argocd-server -n argocd 명령어로 서버를 재시작해주자.

잠시 뒤 ALB가 생성되고 인그레스에 할당된 도메인이 나오면 ArgoCD 웹에 접속할 수 있다.

Spring Boot Sample App 배포

쿠버네티스에 배포하기 위해선 먼저 앱을 컨테이너 이미지로 패키징해야한다.

그리고 컨테이너 이미지를 이미지 저장소에 Push 하여 사용한다.

Terraform에 아래 코드를 추가하고 apply하여 ECR 이미지 저장소를 만들자.

resource "aws_ecr_repository" "ecr_repository" {
  name = "dgyoon/springboot"
  force_delete = true
}

application.propertiesusername, password를 Terraform으로 RDS를 생성할 때 정의한 정보들을 넣어주고, url에는 RDS 클러스터의 라이터 엔드포인트를 넣어준다.

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=myuser
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.show-sql: true

그리고 배스천 호스트로 접속해 RDS로 연결한 후 mydatabase 데이터베이스를 생성해준다.

이 후 Sample App을 jar 파일로 빌드한다.

./gradlew clean build 

그리고 코드의 최상단 경로에 아래 Dockerfile을 생성한다.

FROM openjdk:17.0.2

WORKDIR /springboot

COPY ./build/libs/accessing-data-mysql-0.0.1-SNAPSHOT.jar app.jar

EXPOSE 8080

CMD ["java","-jar","app.jar"]

그리고 컨테이너 이미지를 빌드한다.

docker build -t [ECR_REPO_ADDRESS]/dgyoon/springboot:latest . 

ECR 인증을 마치고 이미지를 Push 한다.

aws ecr get-login-password | docker login --username AWS --password-stdin [ECR_REPO_ADDRESS]

docker push [ECR_REPO_ADDRESS]/dgyoon/springboot:latest

그리고 ArgoCD가 리소스를 생성할 수 있게끔 명시한 Manifest 파일을 생성한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot
  namespace: default
  labels:
    app: springboot
spec:
  selector:
    matchLabels:
      app: springboot
  replicas: 1
  template:
    metadata:
      labels:
        app: springboot
    spec:
      containers:
      - name: springboot
        image: <IMAGE_NAME:TAG>
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
          limits:
            cpu: 100m
            memory: 100Mi
---            
apiVersion: v1
kind: Service
metadata:
  name: springboot
spec:
  selector:
    app: springboot
  type: NodePort
  ports:
  - name: springboot
    protocol: TCP
    port: 8080
    targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: springboot-ingress
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 8080}]'
    alb.ingress.kubernetes.io/target-type: instance
    alb.ingress.kubernetes.io/healthcheck-protocol: HTTP
spec:
  ingressClassName: alb
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: springboot
            port:
              number: 8080

여기서는 Deployment, Service, Ingress 리소스만 생성한다.

그리고 이 파일을 Github Repo에 업로드하고 ArgoCD에서 그 Repo와 연결해준다.

Application → NEW APP 후 아래처럼 명시하고 CREATE하면 Application이 생성된다.

Out of Sync 상태이니 Sync를 한번 해주면 아래와 같이 파드, 서비스, 인그레스가 배포된다.

문서에 적혀있는대로 인그레스의 도메인에 /demo/all 붙여 입력해주면 아래와 출력된다.

그리고 CURL로 API 요청을 보내고 다시 /demo/all 을 확인해보면 추가되어 있는 모습을 볼 수 있다.

$ curl http://[API_INGRESS_DOMAIN]/demo/add -d name=First -d email=someemail@someemailprovider.com
SAVED

배스천 호스트에 접속해 RDS DB로 접속 후 확인해보면 레코드가 추가된 것을 볼 수 있다.

이렇게 RDS와 연결된 Spring Boot APP을 EKS에 배포해 API 애플리케이션 서버를 만들어보았다.

VueJS SPA

이제는 SPA(Single Page Application) Sample을 CloudFront와 S3로 배포해보자.

Terraform으로 CloudFront와 S3 버킷부터 먼저 만들어보자.

지금까지 했듯이 s3 폴더를 만들어 main, variable, output.tf 파일을 작성한다.

modules/s3/main.tf

resource "aws_s3_bucket" "s3_bucket" {
  bucket = var.bucket_name
}

modules/s3/variable.tf

variable "bucket_name" {
  type = string
}

modules/s3/output.tf

output "bucekt_domain_name" {
  value = aws_s3_bucket.s3_bucket.bucket_domain_name
}
output "bucket_name" {
  value = aws_s3_bucket.s3_bucket.bucket
}
output "bucket_id" {
  value = aws_s3_bucket.s3_bucket.id
}

그리고 cloudfront 모듈도 작성하자.
modules/cloudfront/main.tf

resource "aws_cloudfront_distribution" "cloudfront_distribution" {
  origin {
    domain_name = var.domain_name
    origin_id = var.bucket_id
    origin_access_control_id = aws_cloudfront_origin_access_control.s3.id
  }
  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = var.bucket_id
    viewer_protocol_policy = "allow-all"
    cache_policy_id = data.aws_cloudfront_cache_policy.cacheoptimized.id
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
  enabled = true
  default_root_object = "index.html"
  depends_on = [ aws_cloudfront_origin_access_control.s3 ]
}

resource "aws_cloudfront_origin_access_control" "s3" {
  name = "cloudfront_s3_oac"
  origin_access_control_origin_type = "s3"
  signing_behavior = "always"
  signing_protocol = "sigv4"
}

data "aws_cloudfront_cache_policy" "cacheoptimized" {
  name = "Managed-CachingOptimized"
}

resource "aws_s3_bucket_policy" "allow_cloudfront_access" {
  bucket = var.bucket_id
  policy = jsonencode({
        "Version": "2008-10-17",
        "Id": "PolicyForCloudFrontPrivateContent",
        "Statement": [
            {
                "Sid": "AllowCloudFrontServicePrincipal",
                "Effect": "Allow",
                "Principal": {
                    "Service": "cloudfront.amazonaws.com"
                },
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::${var.bucket_name}/*",
                "Condition": {
                    "StringEquals": {
                      "AWS:SourceArn": "${aws_cloudfront_distribution.cloudfront_distribution.arn}"
                    }
                }
            }
        ]
      })
}

modules/cloudfront/variable.tf

variable "domain_name" {
  type = string
}
variable "bucket_id" {
  type = string
}
variable "bucket_name" {
  type = string
}

루트 모듈에서 CloudFront와 S3를 생성해주자.

...
module "s3_web" {
  source = "./modules/s3"
  bucket_name = "sample-dgyoon-web-bucket"
}

module "cloudfront" {
  source = "./modules/cloudfront"
  bucket_id = module.s3_web.bucket_id
  domain_name = module.s3_web.bucekt_domain_name
  bucket_name = module.s3_web.bucket_name
}
...

그리고 VueJS 프로젝트를 생성하고 배포할 수 있게 빌드까지 하자.

npm create vue@latest
cd vue-project
npm install
npm run build

이렇게 하고나면 dist 폴더에 index.htmljs, css 파일이 생길텐데 이 파일들을 전부 방금 생성한 S3 버킷에 전송한다.

cd dist
aws s3 cp ./ s3://sample-dgyoon-web-bucket

그리고 잠시 후 콘솔에 접속해 생성된 CloudFront의 배포 도메인에 접속하면 아래와 같이 나올 것이다.

이렇게 VueJS SPA Sample 웹을 배포해보았다.

마치며

최종적으로 완성하게 된 설계도 되겠다.

눈치챈 분도 있겠지만, 주로 EKS에 배포되는 애플리케이션이 백엔드 개발 API 서버가 되고, CloudFront를 통해 배포되는 웹이 프론트엔드 개발 웹 프로젝트가 되겠다.

해당 웹에서 API 서버의 인그레스 도메인을 통해 서로 통신하고 렌더링하면 하나의 웹 서비스 환경을 구축하게 되는 것이다.

profile
안녕하세요. 방문해주셔서 감사합니다.

0개의 댓글