Github Actions의 의존성 캐싱을 통한 서버 배포속도 향상

Fitpet Developer
18 min readJul 28, 2022

--

많은 분이 CI/CD 속도에 많은 골머리를 앓고 있을 거로 생각합니다. 핏펫에서도 느린 빌드 시간으로 인해 많은 골머리를 앓고 있었습니다. 느린 빌드는 개발자의 피로도를 증가시키게 되고 기다리는 시간이 증가하게 되어 개발하는 속도에도 영향을 미쳐 악순환을 반복하게 됩니다.

서버의 규모가 점점 커지게 되면서 눈덩이처럼 문제가 커져만 갔습니다. 규모가 커져 속도는 더욱 느려졌고 보다 많아진 개발자분들이 불편함을 호소하게 되었습니다.

예를 들어 5분 정도의 배포 시간이 걸린다고 가정합니다. 하루에 30번 배포를 진행한다고 했을 때 대략 150분이 소요됩니다. 단순히 배포만 하는 데 있어 2시간이 넘는 시간을 쏟게 됩니다.

위와 같은 개발자가 겪고 있는 악순환을 제거 제거하기 속도 개선을 진행하고자 하였습니다.

TL;DR

Github Actions의 종속성 캐싱을 통하여 빌드속도 향상

테스트 환경

저희 서버와 비슷한 환경을 구축하기 위해 테스트 서버를 구축하였습니다.

현재는 AWS ECS로 제공되고 있습니다. ECS는 정말 간단하고 쉽게 Container Orchestration을 제공할 수 있는 서비스로서 빠르게 서버를 제공할 수 있는 좋은 서비스입니다.

전체적으로 간단한 Java 코드를 생성 후 Terraform으로 서버 환경 구축 후 GitHub Actions로 배포하는 구조를 만들어 보았습니다.

모든 코드는 아래 Github를 통해 확인 가능합니다.

https://github.com/b100to/blog-test

Architecture

테스트 프로젝트 생성

  • Terraform
  • AWS ECS Fargate
  • Java11 — Gradle
  • Git Actions

IaC, Terraform을 통한 ECS 테스트 환경 구성

테라폼을 통하여 간단하게 테스트 환경을 구성하였습니다.

Terraform Code

version.tf

versions.tf# AWS 프로바이더 선언 및 리전, 프로파일 세팅
provider "aws" {
region = "ap-northeast-2"
profile = "dev"
}
#AWS 프로바이더 버전 지정
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.22.0"
}
}
}

data.tf

data.tf#AWS ID
data "aws_caller_identity" "current" {}
#기존 VPC
data "aws_vpc" "this" {
id = var.vpc_id
}
#기존 퍼블릭 서브넷a
data "aws_subnet" "public1" {
id = var.public_subnet1
}
#기존 퍼블릭 서브넷c
data "aws_subnet" "public2" {
id = var.public_subnet2
}
#기존 프라이빗 서브넷a
data "aws_subnet" "private1" {
id = var.private_subnet1
}
#기존 프라이빗 서브넷c
data "aws_subnet" "private2" {
id = var.private_subnet2
}
#기존 SG
data "aws_security_group" "this" {
id = var.sg_id
}

networks.tf

networks.tf#TG 생성 - 서비스 핼스체크 및 private IP 테이블 역할(?) 담당
resource "aws_lb_target_group" "this" {
name = "test-blog123"
vpc_id = data.aws_vpc.this.id
target_type = "ip"
load_balancing_algorithm_type = "round_robin"
port = 80
deregistration_delay = "5"
protocol = "HTTP"
protocol_version = "HTTP1"
slow_start = 0
tags = {}
health_check {
protocol = "HTTP"
port = "traffic-port"
path = "/"
enabled = true
matcher = "200"
interval = 5
timeout = 3
healthy_threshold = 2
unhealthy_threshold = 2
}
stickiness {
cookie_duration = 86400
enabled = false
type = "lb_cookie"
}
}
#ALB 생성 - 자체 생성 된 DNS를 통하여 HTTP로 외부와 인터넷 연결 가능
resource "aws_lb" "this" {
name = "test-blog123"
load_balancer_type = "application"
ip_address_type = "ipv4"
desync_mitigation_mode = "defensive"
subnets = [
data.aws_subnet.public1.id
, data.aws_subnet.public2.id
]
security_groups = [
data.aws_security_group.this.id
]
idle_timeout = 60
drop_invalid_header_fields = false
enable_deletion_protection = false
enable_http2 = true
enable_waf_fail_open = false
internal = false
tags = {}
}
#LB 리스너 생성 - LB과 TG을 연결 시켜준다.
resource "aws_lb_listener" "this" {
load_balancer_arn = aws_lb.this.arn
port = 80
protocol = "HTTP"
tags = {}
default_action {
target_group_arn = aws_lb_target_group.this.arn
type = "forward"
}
}

main.tf

main.tf#ECS 클러스터 생성
resource "aws_ecs_cluster" "this" {
name = "blog-test"
tags = {}
tags_all = {}
setting {
name = "containerInsights"
value = "disabled"
}
}
#ECR 생성
resource "aws_ecr_repository" "this" {
image_tag_mutability = "MUTABLE"
name = "blog-test"
tags = {}
image_scanning_configuration {
scan_on_push = false
}
}
#테스크 정의 생성 - Task 기본 스펙 설정
resource "aws_ecs_task_definition" "this" {
family = "test-blog"
cpu = "512"
memory = "1024"
network_mode = "awsvpc"
runtime_platform {
cpu_architecture = "X86_64"
operating_system_family = "LINUX"
}
requires_compatibilities = [
"FARGATE",
]
task_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/CloudEcsTaskRole"
execution_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ecsTaskExecutionRole"
container_definitions = jsonencode(
[
{
name = "app"
image = "sha256:72b7e95ac29f46e2e53a97413fcfdfe3f7599136d5bc745ffb60a65f464d565e"
portMappings = [
{
containerPort = 8080
hostPort = 8080
protocol = "tcp"
},
]
cpu = 0
environment = []
essential = true
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = "/ecs/test-blog"
awslogs-region = "ap-northeast-2"
awslogs-stream-prefix = "ecs"
}
}
mountPoints = []
volumesFrom = []
},
]
)
tags = {
"ecs:taskDefinition:createdFrom" = "ecs-console-v2"
"ecs:taskDefinition:stackId" = "arn:aws:cloudformation:ap-northeast-2:${data.aws_caller_identity.current.account_id}:stack/ECS-Console-V2-TaskDefinition-4bcedecd-24f5-4570-8d0c-936d1acd52cc/1c346740-fcd6-11ec-bfdb-02068544a002"
}
}
#ECS 서비스 생성
resource "aws_ecs_service" "this" {
name = "test-blog123"
cluster = aws_ecs_cluster.this.arn
iam_role = "aws-service-role"
desired_count = 1
health_check_grace_period_seconds = 60
deployment_minimum_healthy_percent = 50
deployment_maximum_percent = 200
task_definition = "${aws_ecs_task_definition.this.id}:${aws_ecs_task_definition.this.revision}"
tags = {}
# 스팟 비중을 어느정도 설정할지 지정합니다.
capacity_provider_strategy {
base = 0
capacity_provider = "FARGATE_SPOT"
weight = 1
}
# LB 세팅
load_balancer {
container_name = "app"
container_port = 8080
target_group_arn = aws_lb_target_group.this.arn
}
# 네트워크 세팅
network_configuration {
assign_public_ip = true
security_groups = [
data.aws_security_group.this.id
]
subnets = [
data.aws_subnet.private1.id,
data.aws_subnet.private2.id
]
}
}

Java DockeFile

간단히 Intelli를 통하여 많이들 사용하시는 Gradle 빌드 도구를 이용하여 Java HelloWorld를 생성하였습니다.

이후 Dockerfile을 이용하여 Dockerizing 작업을 진행하였습니다.

Dockerfile

#상용에서는 빌드 속도를 위해 Alpine 이미지을 사용합니다.
FROM openjdk:11
RUN addgroup --system spring && adduser --system spring
USER spring:spring
COPY ./build/libs/*-SNAPSHOT.jar app.jarEXPOSE 8080ENTRYPOINT ["java","-jar","/app.jar"]

Github Actions를 통하여 AWS ECS CI/CD 속도 향상 시키기.

Github Actions는 Workflow가 매번 새로 셋업이 됩니다. 그로 인해 로컬과 달리 캐시 지원을 할 수 있도록 세팅을 해줘야 합니다.
로컬에서 작업하는 것과 같이 캐시를 사용하려면 Github의 cache Action을 사용해야 합니다. 이 Action은 고유키를 이용하여 캐시를 식별하고 가져옵니다.

사용 예시

deploy_test.yml

name: deploy_teston:
push:
branches:
- main #main 브랜치에서 시작.
#UI에서 재시작 가능.
workflow_dispatch:
#전역 변수 지정
env:
ECS_CLUSTER_NAME: blog-test
ECS_SERVICE_NAME: test-blog123
IMAGE_NAME: blog-test
jobs:
Build:
runs-on: ubuntu-latest # 사용할 OS를 선택합니다.
steps:
- uses: actions/checkout@v3
# 사용할 Gradle 설치 및 버전 지정합니다.
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
gradle-version: wrapper
# JDK 버전 및 Gradle 빌드합니다.
- name: Setup Java JDK
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
cache: gradle # 종속성 제거를 위한 캐쉬 지원 가능.
- run: ./gradlew --no-daemon build --stacktrace
# Deploy Job에서 넘겨 받도록 업로드합니다.
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: app
path: build/libs
Deploy:
runs-on: ubuntu-latest
needs: build # Build Job먼저 작업 후 진행하도록 합니다.
steps:
- name: Checkout
uses: actions/checkout@v3
# 위 빌드한 Build job에서 Jar파일을 가저옵니다.
- name: Download app
uses: actions/download-artifact@v2
with:
name: app
path: build/libs
# Docker Builx를 사용하도록 지정합니다.
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
with:
install: true

# 고유키를 생성합니다.
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
# ECR에 로그인 할 수 있도록 Credentials를 지정합니다.
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
# ECR 로그인
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
# Short SHA를 생성합니다.
- uses: benjlevesque/short-sha@v1.2
id: short-sha
with:
length: 6
- run: echo $SHA
env:
SHA: ${{ steps.short-sha.outputs.sha }}
- run: echo $SHA
env:
SHA: ${{ env.SHA }}

# docker Build 및 Push를 합니다.
- name: Build and push
id: docker-build
uses: docker/build-push-action@v2
with:
push: true # 이미지를 푸쉬합니다.
tags: ${{ steps.login-ecr.outputs.registry }}/${{ env.IMAGE_NAME }}:${{ env.SHA }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
# 오래된 캐쉬를 날립니다.
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
# 기본 Task Definition 탬플릿을 지정하고, Tags 자동으로 변경합니다.
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: app
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.IMAGE_NAME }}::${{ env.SHA }}
# Task-Definition을 최신으로 변경 업데이트 합니다.
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE_NAME }}
cluster: ${{ env.ECS_CLUSTER_NAME }}
wait-for-service-stability: false
#ECS Task가 정상적으로 생성되기까지 기다립니다.
#Task가 생성 되는 거까지 확인하고자 하면 True 지정하시면 됩니다.

Github Actions Cache 사용 유무 속도 차이

Gradle Build

55s15s

Cache 적용 전
Cache 적용 후

Docker Build & Push

30s → 27s

Cache 적용 전
Cache 적용 후

테스트 환경에선 크게 속도 차이를 느낄 수 없지만, 실질적으로 실 서버에 적용했을 때 약 2배 정도의 차이를 보여주었습니다.

개발자분들이 느끼는 불편함이 이전보다 줄어들었습니다. 또한 배포에 부담감을 덜 수 있게 되었습니다. 이 글을 통해 저희와 같이 CI/CD 속도로 고민하시는 분들에게 도움이 되었기를 기대합니다.

감사합니다.

written by Anton (Jonghwa Baek)

email: jh.baek@fitpet.co.kr

--

--