fly.io 는 백엔드 서비스를 배포하는 가장 쉽고 편리한 방법입니다. fly.io를 통해서 URL 단축 서비스를 배포할 수 있다.
자바로 만든 결과물(jar 파일)은 그 자체로 실행될 수 없습니다. JVM 이라는 환경에서 실행이 됩니다. 배포라는 것은 서버(일반 컴퓨터와 같음)에서 우리가 만든 결과물(jar 파일)을 실행하는 것 입니다. 서버를 세팅한다는 것은 서버에 JVM와 여러가지 프로그램들을 미리 설치해두는 행위를 말합니다. 즉, 서버에서 실행될 프로그램이 필요로하는 것을 미리 설치하는 행위가 바로 서버세팅입니다. 도커이미지는 제품(자바로 만든 결과물)을 감싸는 포장지라고 볼 수 있습니다. 그 포장지 안에는 JVM 같이 해당 프로그램이 필요한 환경, 인프라까지 같이 동봉할 수 있습니다. 도커 이미지를 이용해서 배포하면 운영서버에서 (거의) 무조건 실행됨을 보장할 수 있습니다. 도커를 이용하면 서버에 도커를 설치하는 것 이외에 딱히 미리 설치해둘 프로그램이 없습니다. 굉장히 편해집니다.

Dockerfile 로 N개의 도커이미지를 만들 수 있고, 도커 이미지로 N개의 컨테이너를 만들 수 있습니다.
도커 이미지는 [밀키트]와 비슷하지만 훨씬 더 편리합니다. 비유하자면, 밀키트는 밀키트인데 조리도구(가스레인지, 냄비 등)까지 포함된 밀키트라고 보시면 됩니다. 그래서 현대의 웹 서비스에서 주류기술로 자리잡았습니다.
컨테이너(Container) [요리]
: 컨테이너는 애플리케이션과 그 필요한 모든 것을 포함하는 격리된 환경입니다. 가볍고 빠르게 시작되며, 다른 컨테이너나 호스트 시스템과의 충돌 없이 독립적으로 실행됩니다. 이는 개발, 배포, 실행을 일관되게 만들어 줍니다.
도커 파일(Dockerfile) [레시피]
: 도커 파일은 도커 이미지를 빌드하기 위한 설정 파일로, 이미지 생성 과정에 필요한 명령어를 순서대로 담고 있습니다. 이 파일을 사용하면 이미지 빌드 과정을 자동화하고, 재사용 가능한 방식으로 관리할 수 있습니다.
이미지(Image)
: 이미지는 컨테이너를 생성하는 데 사용되는 템플릿으로, 애플리케이션 실행에 필요한 코드, 라이브러리, 환경설정 등이 포함됩니다. 읽기 전용이며, 컨테이너는 이 이미지를 기반으로 생성됩니다.
도커 데몬(Docker Daemon)
: 도커 데몬은 컨테이너의 생성, 실행, 모니터링, 삭제 등을 관리하는 백그라운드 프로세스입니다. 도커 클라이언트와 통신하며, 도커의 모든 중요 작업을 담당합니다.
도커 허브(Docker Hub)
: https://hub.docker.com/
도커 허브는 도커 이미지를 저장하고 공유할 수 있는 클라우드 서비스입니다. 사용자는 자신의 이미지를 업로드하여 공유할 수 있으며, 다른 사람이 만든 이미지를 검색하고 사용할 수 있습니다.

도커 데스크톱 설치

docker 설치가 잘 된 것:

스프링부트 앱 vs 떡볶이 밀키트

모든 도커자원(이미지, 컨테이너, 네트워크 볼륨) 삭제 명령어
//컨테이너 삭제
docker rm -f $(docker ps -qa)
//이미지 삭제
docker rmi -f $(docker images -qa)
//안쓰는 네트워크 삭제
docker network prune -f
//안쓰는 볼륨 삭제
docker volume prune -f
HTML 파일을 원격지의 브라우저에게 전송하려면 웹서버가 필요합니다.
: 라면요리에 가스레인지가 필요하듯이, HTTP 전송에는 웹서버(NGINX 등)이 필요합니다.
웹서버중에서 유명한 것이 nginx 입니다. 우리는 nginx 를 사용할 예정입니다. nginx 도커 이미지는 이미 도커 허브에 있습니다.
우리는 도커 이미지를 만드는게 목표입니다.
: 도커 이미지는 Dockerfile 로 부터 생성됩니다.
Dockerfile 작성
: 도커허브의 nginx:latest 를 기반으로 합니다.
소스코드 생성
: index.html 파일 생성
도커 이미지 생성
: docker build -t 이미지이름 .
도커 이미지 이름 : 레포지터리이름:태그 // 기본 태그는 latest
참고로 태그를 latest 로 하고 싶다면 생략가능 // EX : docker build -t nginx-1 .
위 명령은 docker build -t nginx-1:latest . 와 같다.
생성된 도커 이미지 확인
: docker images
HTML 앱과 스프링부트 앱 비교

index.html
<h1>Hello</h1>
Dockerfile
# 기본 이미지 설정
FROM nginx:latest
# index.html 파일 복사
COPY index.html /usr/share/nginx/html/
#Ngnix 설정 파일 복사
COPY ngnix.conf /etc/ngnix/conf.d/default.conf
# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]
실습 과정



도커 이미지 삭제하는 방법
docker rmi 이미지이름
도커 이미지 이름 : 레포지터리이름:태그// 도커 이미지의 이름은 도커 레포지터리명과 태그의 조합(레포지터리명:태그) 입니다.
-기본 태그는 latest
-참고 태그를 latest 로 하고 싶다면 생략가능
EX : docker rmi nginx-1
-위 명령은 docker rmi nginx-1:latest . 와 같다.
id 같아서 안될떈 이름으로 지우면 됨

도커 이미지로 도커 컨테이너를 생성할 수 있습니다.(다른 말로 도커 이미지를 실행하면 도커 컨테이너가 생성됩니다.) 도커 이미지는 일종의 템플릿이고 이력서 양식과 같습니다. 도커 컨테이너는 도커 이미지를 디스크에서 복사하여 메모리에 띄운 것과 같습니다. 마치 우리가 특정 회사를 위한 이력서를 작성할 때 원본 이력서 양식을 보존하기 위해서 이력서 양식 원본에서 바로 편집하지 않고 그것의 복사본을 만든 후 작업하는 것과 같습니다.
80 포트를 사용하는 nginx-1-1 컨테이너 띄웁니다.
docker run -d nginx-1 -p 80:80 --name 컨테이너이름 이미지이름
-p 80:80 에서 왼쪽은 HOST 포트이고, 오른쪽은 도커 컨테이너의 포트이다. 이 옵션으로 우리가 해당 NGINX에 접근이 가능해진다.
EX : docker run -d --name nginx-1-1 -p 80:80 nginx-1
이렇게 수행하면 다른 컨테이너에서는 80 포트를 사용할 수 없습니다.
이렇게 수행하면 다른 컨테이너에서는 nginx-1-1 이라는 이름을 사용할 수 없습니다.
8081 포트를 사용하는 nginx-1-2 컨테이너 띄웁니다.
docker run -d nginx-1 -p 8081:80 --name 컨테이너이름 이미지이름
EX : docker run -d --name nginx-1-2 -p 8081:80 nginx-1
이렇게 수행하면 다른 컨테이너에서는 8081 포트를 사용할 수 없습니다.
이렇게 수행하면 다른 컨테이너에서는 nginx-1-2 이라는 이름을 사용할 수 없습니다.
하나의 도커 이미지로 2개의 컨테이너를 띄웠고 각가의 컨테이너가 같은 이름을 가질 수 없고 같은 포트를 점유할 수 없어서 서로 다른 이름, 다른 포트를 할당했습니다.
도커 컨테이너를 삭제
docker rm -f 컨테이너_이름
EX : docker rm -f nginx-1-1
EX : docker rm -f nginx-1-2
실습 과정



컨테이너가 이미 완성이 된 것을 확인


docker 컨테이너 삭제

fly.io 는 서버(PC)를 제공해줍니다. 서버는 일반적인 PC와 같은 컴퓨터 입니다. 다만 서버에는 공인 IP가 부여되어 있습니다(필수). 보통 웹서버에는 도메인(IP의 별칭)이 부여되어 있습니다. 웹서버는 서버중에서 웹 서비스를 제공하는 서버를 말합니다. flyctl을 설치하면 컴퓨터에서 명령어로 fly.io 에 여러가지 서비스를 이용할 수 있습니다. flyctl을 설치 후 로그인해야 합니다.

실습 과정
fly 설치

fly login

fly.io 에 서비스를 배포하려면 앱 이라는 것을 생성해야 합니다. fly.io 에서는 하나의 프로그램 및 서비스를 앱 이라는 명칭으로 부릅니다. 앱은 fly.io 에서 서비스의 단위를 말합니다. 만약에 우리가 5개의 서비스를 배포하려 한다면 5개의 앱을 만들어야 합니다.
앱은 fly launch --no-deploy 명령으로 생성합니다.
앱은 대시보드(https://fly.io/dashboard) 에서 확인 가능합니다.
생성한 폴더에 fly.toml 파일도 생깁니다. 해당 파일은 앱의 설정을 바꿀때 사용됩니다.
아직은 우리가 fly deploy 명령을 내리기 정이기 때문에 앱은 현재 Pending 상태 입니다.
실습 과정



3가지가 있다면 fly.io 배포가 가능합니다.
3가지 요소 : 프로젝트 폴더, Dockerfile, fly.toml
fly.toml 만 있으면 안되고, fly.toml 에 대응되는 fly.io 앱이 존재해야 합니다.
프로젝트 폴더에서 fly deploy 명령을 입력하면 프로젝트 폴더의 모든 파일들이 fly.io 앱으로 복사됩니다. 빌더 앱이 생성되고 업로드된 파일들로 도커 빌드가 진행됩니다. 도커이미지가 완성됩니다. 기계들(보통 2개)에 컨테이너들이 위치하게 됩니다. 여기서 기계는 머신, PC, 서버를 의미 합니다. 기계마다 컨테이너가 하나씩 위치합니다.
도메인은 https://앱_이름.fly.dev 입니다.
EX : https://jhs512-nginx-1.fly.dev
소스코드가 바뀌면 재배포를 해주세요.
재배포 명령어 : fly deploy
기계들(보통 2개)에서 돌아가고 있는 기존 컨테이너들이 제거되고, 새 도커이미지로 만든 새 컨테이너들이 그 자리를 차지 합니다. 기존 컨테이너가 모두 꺼지고, 새 컨테이너가 그 자리를 차지하게 되면 만약에 단순하게 교체를 하면 고객 입장에서 중단타임이 발생합니다. 즉 고객이 우리 웹서비스를 이용하는 중에 오류(404 등)을 경험하게 되는 구간이 생깁니다. 다행스럽게도, fly.io는 기본적으로 롤링 업데이트라는 무중단 배포(교체) 방식을 사용합니다. fly.io 를 통한 배포는 기본적으로 무중단이라고 보셔도 됩니다.
실습 과정




코파일럿 플러그인 업데이트 코파일런 사용을 추천드립니다. 단 유료입니다.
실습이 끝났다면 요금을 절약하기 위해서 앱을 지워주셔야 합니다. 요청이 한동안 없다면 기본적으로는 앱이 자동으로 꺼지기 때문에 앱을 지우지 않아도 비용이 아주 저렴하게 발생합니다.
Dockerfile 파일을 생성합니다. 스프링부트에 맞게 세팅해야 합니다.
그 다음 fly launch --no-deploy 명령을 통해서 fly.io 앱을 만들고 fly.toml 파일을 생성합니다. 이 과정에서 danieljjs-surl 앱이 생성됩니다.
도메인 : https://danieljjs-surl.fly.dev
HomeController 를 추가해서 GET / 요청에 응답하도록 작업
프로젝트 폴더에서 fly deploy 명령을 입력하면 프로젝트 폴더의 모든 파일들이 fly.io 앱으로 복사된 후 빌드됩니다. fly.io 안에서 도커 이미지가 생성되었습니다. 해당 도커 이미지를 기반으로 2개의 머신에 컨테이너가 생깁니다.
브라우저의 요청이 있을 때 마다 2개의 기계 중 랜덤으로 1개가 선택되어 그 안에 있는 컨테이너안에 있는 스프링부트가 작동합니다. 일종의 부하분산 이라고 보시면 됩니다.
New Dockerfile
# 첫 번째 스테이지: 빌드 스테이지
FROM gradle:jdk21-graal-jammy AS builder
# 작업 디렉토리 설정
WORKDIR /app
# 소스 코드와 Gradle 래퍼 복사
COPY build.gradle .
COPY settings.gradle .
# Gradle 래퍼에 실행 권한 부여
RUN gradle wrapper
# 종속성 설치
RUN ./gradlew dependencies --no-daemon
# 소스 코드 복사
COPY src src
# 애플리케이션 빌드
RUN ./gradlew build --no-daemon
# 두 번째 스테이지: 실행 스테이지
FROM container-registry.oracle.com/graalvm/jdk:21
# 작업 디렉토리 설정
WORKDIR /app
# 첫 번째 스테이지에서 빌드된 JAR 파일 복사
COPY --from=builder /app/build/libs/*.jar app.jar
# 실행할 JAR 파일 지정
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
실습 과정
Dockefile 생성

fly launch --no-deploy 실행

HomeController.java 클래스를 추가하면 fly.dev에서 실행되는 것을 확인할 수 있다.
package com.ll.demo03;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HomeController {
@GetMapping("/")
@ResponseBody
public String showMain(){
return "Hello World!";
}
}

개발환경(dev)과 운영환경(prod)은 서로 다릅니다. prod 는 production 의 약자입니다.
각 환경에 맞는 설정(application.yml)이 필요합니다. 다행히도 스프링부트에서는 그 기능을 지원합니다. application.yml 은 모든환경에서 작동합니다.
application-dev.yml 는 개발환경에서만 작동합니다.
application.yml 의 spring.profiles.active=dev 설정 덕분에 작동합니다.
application-prod.yml 는 운영환경에서만 작동합니다.
Dockerfile 의 -Dspring.profiles.active=prod 옵션 덕분에 작동합니다.
application.yml 이 제일 먼저 작동합니다. 제일 먼저 작동하는 파일이 우선순위가 가장 낮습니다. 나중에 작동하는 설정파일의 정보가 우선적으로 적용됩니다. 프로젝트 폴더에서 fly deploy 명령을 입력하여 바뀐 소스코드가 실서버(운영서버)에 반영되도록 작업

개발환경과 운영환경을 분리한다.


깃허브 리포지터리에 푸시한 것 만으로 자동으로 배포까지 이루어지면 굉장히 편합니다. 이것을 CI/CD 라고 합니다. 사실 fly.io 가 무중단이기 때문에 우리의 프로젝트에는 무중단 CI/CD 가 걸려 있다 라고 이야기 해도 됩니다. GITHUB ACTION 을 걸어두면 특정 이벤트가 발생했을 때 어떠한 일이 작동하도록 할 수 있습니다. GITHUB ACTION 은 우리가 매번 fly deploy 명령을 입력하지 않아도 되도록 해줍니다. 이것을 CI/CD 라고 합니다.
이벤트 EX : 특정 브랜치에 특정 파일이 변경되는 푸시가 발생
소스코드의 내용중에서 GITHUB 에 노출되면 안되는 내용도 있습니다. 그런 것은 application-secret.yml 에 몰아두고 .gitignore 에 src/main/resources/application-secret.yml 를 추가하여 해당파일이 GIT 에 저장되지 않도록 해야 합니다. application-secret.yml 은 모든환경에서 작동합니다. application.yml 의 spring.profiles.include=secret 설정 덕분에 작동합니다.
그런데 우리의 목표는 도커 빌드를 fly.io 에서 수행합니다. 빌드라는 행위는 완제품을 만드는 행위입니다. 그렇다면 빌드를 할 때 application-secret.yml 이 꼭 필요합니다. 그런데 github 리포지터리에는 application-secret.yml 파일이 있어야 합니다. 하지만 우리는 보안 때문에 해당 파일이 오직 로컬에만 존재하도록 설정했습니다.
우리는 application-secret.yml 을 암호화 해서 해당 리포지터리에 넣어야 합니다. 깃허브에는 시크릿 변수라는 기능을 제공합니다. 리는 application-secret.yml 의 내용을 시크릿 변수에 저장을 하면 됩니다.
.github/workflows/deploy.yml 파일 추가 .github/workflows 폴더명은 지켜야 하는 규칙입니다. 그래야 이벤트가 걸립니다. GITHUB 리포지터리에 이벤트를 건다라고 보통 표현합니다. 위 파일을 추가하면 이벤트가 걸립니다. deploy.yml 에는 언제 해당 파일이 실행되어야 하는지와
실행되면 무슨일이 벌어져야 하는지가 기술되어 있습니다.
리포지터리 세팅을 해야 합니다. 시크릿변수 APPLICATION_SECRET_YML 를 생성해 주세요. 시크릿변수 FLY_API_TOKEN 을 생성해 주세요. 키를 안전한곳에(본인만 볼 수 있는 곳)에 메모해 두는것을 추천합니다.
Workflow permissions
Choose the default permissions granted to the GITHUB_TOKEN when running workflows in this repository. You can specify more granular permissions in the workflow using YAML. Learn more about managing permissions.
체크 : Read and write permissions
해당내용은 추후에 AWS 를 위한 배포를 위해 필요합니다. 사실 지금방식(fly.io 배포)에서는 필요하지 않습니다. 해당 내용을 활성화하면 GITHUB ACTION 이 해당 리포지터리에 태그나 릴리즈를 생성할 권한을 얻습니다.
GITHUB PUSH 이제 fly deploy 를 명시적으로 할 필요가 없습니다. GITHUB 로 푸시하는 것 만으로 자동으로 배포까지 진행됩니다. 무중단 CI/CD 완성
.gitignore
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Custom ###
src/main/resources/application-secret.yml
db_dev.mv.db
db_dev.trace.db
src/main/generated
.github/workflows/deploy.yml
name: Fly Deploy
on:
push:
paths:
- settings.gradle
- build.gradle
- src/**
- fly.toml
- Dockerfile
- .github/workflows/deploy.yml
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: application-secret.yml 생성
env:
APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET_YML }}
run: echo "$APPLICATION_SECRET" > src/main/resources/application-secret.yml
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
실습 과정
secretKey 추가, secret.yml 추가
package com.ll.demo03;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HomeController {
@Value("${custom.site.name}")
private String siteName;
@Value("${custom.secret.key}")
private String secretKey;
@GetMapping("/")
@ResponseBody
public String showMain(){
return "Hello World!, " + siteName;
}
@GetMapping("/secretKey")
@ResponseBody
public String showSecretKey(){
return "secretKey : " + secretKey;
}
}


git 에는 없지만 빌드에 꼭 필요한 파일은 이런식으로 시크릿 변수로 만든다.


TOKEN은 절대로 다른사람에게 보여줘서는 안된다.

소스코드를 변경하고 그것을 실서버에 반영하는 방법
CI/CD가 구축되었기 때문에 이제 GIT 리포지터리에 PUSH 만 하면 됩니다. 그런데 만약 application-secret.yml 의 내용을 바꾸고 그것을 서버에 반영해야 한다면 리포지터리의 시크릿변수 APPLICATION_SECRET_YML 의 내용을 변경한 후 푸시를 하거나 해당 액션을 명시적으로 재시도 하셔야 합니다.
배포된 앱이 잘 되는지 테스트
https://danieljjs-surl.fly.dev/s/내용/URL
EX : GET https://danieljjs-surl.fly.dev/s/네이버/https://www.naver.com

EX : GET https://danieljjs-surl.fly.dev/s/다음/https://www.daum.net

https://danieljjs-surl.fly.dev/all

EX : GET https://danieljjs-surl.fly.dev/all
https://danieljjs-surl.fly.dev/g/URL_번호
EX : GET https://danieljjs-surl.fly.dev/g/1

EX : GET https://danieljjs-surl.fly.dev/g/2

fly.io 의 기본적인 앱은 일정기간동안 요청이 없으면 자동으로 꺼집니다. 앱이 꺼질때 스프링부트 내부의 변수에 저장되어 있던 URL 정보들이 전부 날아갑니다. 이 문제는 이후에 영속성을 부여해서 해결할 예정입니다. 이렇게 필요없는 상황에서 앱이 자동으로 꺼지기 때문에 fly.io 가 비용이 저렴하게 발생합니다. 물론 꺼진 이후에 요청이 들어오면 자동으로 켜집니다. 그래서 5분동안 요청이 없다가, 그 이후에 요청을 하면 해당 요청은 오래 걸립니다.