[AWS] SpringBoot 프로젝트 EC2 중단 배포하기

@t189216·2024년 4월 17일

🚢 DevOps

목록 보기
5/6

AWS에서 서버를 생성하는 방법에는 여러가지가 있습니다.

  • 웹에서 마우스로 직접 클릭
  • AWS CLI를 사용해 명령어로 생성
  • Terraform를 사용

여기서는 Terraform과 Docker를 사용해 무중단 배포를 해보겠습니다.

Terraform?

인프라 자동화 툴 중 하나로, 원하는 인프라를 코드로 서술할 수 있습니다.

1. terraform 설정

프로젝트와 다음과 같은 파일들을 생성합니다.

terraform/variables.tf

Terraform에서 사용되는 변수들을 정의하는 부분입니다. 코드의 유연성과 재사용성을 위해 사용되며, Terraform 코드 내에서 ${var.prefix}, ${var.region}, ${var.nickname}와 같은 형식으로 참조될 수 있습니다.

variable "prefix" {
  description = "Prefix for all resources"
  default     = "dev"
}

variable "region" {
  description = "region"
  default     = "ap-northeast-2"
}

variable "nickname" {
  description = "nickname"
  default     = "t189216"
}

terraform/main.tf

AWS 인프라를 구성합니다. AWS 버전, VPC, 인터넷 게이트웨이, 라우팅 테이블, 보안 그룹, EC2, IAM 등을 설정합니다.

terraform {
  # AWS 라이브러리 import
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

# AWS 설정 시작
provider "aws" {
  region = var.region
}
# AWS 설정 끝

# VPC 설정 시작
resource "aws_vpc" "vpc_1" {
  cidr_block = "10.0.0.0/16"

  # DNS 지원을 활성화
  enable_dns_support = true
  # DNS 호스트 이름 지정을 활성화
  enable_dns_hostnames = true

  tags = {
    Name = "${var.prefix}-vpc-1"
  }
}

resource "aws_subnet" "subnet_1" {
  vpc_id                  = aws_vpc.vpc_1.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "${var.region}a"
  map_public_ip_on_launch = true # 이 서브넷이 배포되는 인스턴스에 공용 IP를 자동으로 할당

  tags = {
    Name = "${var.prefix}-subnet-1"
  }
}

resource "aws_subnet" "subnet_2" {
  vpc_id                  = aws_vpc.vpc_1.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "${var.region}b"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.prefix}-subnet-2"
  }
}

resource "aws_subnet" "subnet_3" {
  vpc_id                  = aws_vpc.vpc_1.id
  cidr_block              = "10.0.3.0/24"
  availability_zone       = "${var.region}c"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.prefix}-subnet-3"
  }
}

# AWS 인터넷 게이트웨이
resource "aws_internet_gateway" "igw_1" {
  vpc_id = aws_vpc.vpc_1.id

  tags = {
    Name = "${var.prefix}-igw-1"
  }
}

# AWS 라우트 테이블
resource "aws_route_table" "rt_1" {
  vpc_id = aws_vpc.vpc_1.id

  # 라우트 규칙을 설정. 모든 트래픽(0.0.0.0/0)'igw_1' 인터넷 게이트웨이로 보냄
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw_1.id
  }

  tags = {
    Name = "${var.prefix}-rt-1"
  }
}

# 라우트 테이블과 서브넷을 연결
resource "aws_route_table_association" "association_1" {
  subnet_id      = aws_subnet.subnet_1.id
  route_table_id = aws_route_table.rt_1.id
}

resource "aws_route_table_association" "association_2" {
  subnet_id      = aws_subnet.subnet_2.id
  route_table_id = aws_route_table.rt_1.id
}

resource "aws_route_table_association" "association_3" {
  subnet_id      = aws_subnet.subnet_3.id
  route_table_id = aws_route_table.rt_1.id
}

# AWS 보안 그룹 리소스
resource "aws_security_group" "sg_1" {
  name = "${var.prefix}-sg-1"

  # 인바운드 트래픽 규칙
  ingress {
    from_port = 0
    to_port   = 0
    protocol = "all"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # 아웃바운드 트래픽 규칙
  egress {
    from_port = 0
    to_port   = 0
    protocol = "all"
    cidr_blocks = ["0.0.0.0/0"]
  }

  vpc_id = aws_vpc.vpc_1.id

  tags = {
    Name = "${var.prefix}-sg-1"
  }
}

# EC2 설정 시작

# EC2 역할 생성
resource "aws_iam_role" "ec2_role_1" {
  name = "${var.prefix}-ec2-role-1"

  # 이 역할에 대한 신뢰 정책 설정. EC2 서비스가 이 역할을 가정할 수 있도록 설정
  assume_role_policy = <<EOF
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "",
        "Action": "sts:AssumeRole",
        "Principal": {
            "Service": "ec2.amazonaws.com"
        },
        "Effect": "Allow"
      }
    ]
  }
  EOF
}

# EC2 역할에 AmazonS3FullAccess 정책을 부착
resource "aws_iam_role_policy_attachment" "s3_full_access" {
  role       = aws_iam_role.ec2_role_1.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}

# EC2 역할에 AmazonEC2RoleforSSM 정책을 부착
resource "aws_iam_role_policy_attachment" "ec2_ssm" {
  role       = aws_iam_role.ec2_role_1.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
}

# IAM 인스턴스 프로파일 생성
resource "aws_iam_instance_profile" "instance_profile_1" {
  name = "${var.prefix}-instance-profile-1"
  role = aws_iam_role.ec2_role_1.name
}

locals {
  ec2_user_data_base = <<-END_OF_FILE
#!/bin/bash
yum install docker -y    # Docker 설치
systemctl enable docker  # Docker 부팅 시 자동 시작 설정
systemctl start docker   # Docker 서비스 시작

curl -L https:#github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose  # 최신 Docker Compose 다운로드 및 설치
chmod +x /usr/local/bin/docker-compose  # Docker Compose 실행 권한 부여

yum install git -y  # Git 설치

sudo dd if=/dev/zero of=/swapfile bs=128M count=32  # 4GB 스왑 파일 생성
sudo chmod 600 /swapfile  # 스왑 파일 권한 변경
sudo mkswap /swapfile  # 스왑 파일을 스왑 공간으로 설정
sudo swapon /swapfile  # 스왑 파일 활성화
sudo swapon -s  # 활성화된 스왑 파일 목록 확인
sudo sh -c 'echo "/swapfile swap swap defaults 0 0" >> /etc/fstab'  # 부팅 시 스왑 파일 자동 활성화 설정

END_OF_FILE
}

# EC2 인스턴스 생성
resource "aws_instance" "ec2_1" {
  ami                         = "ami-0c031a79ffb01a803" # Amazon Linux 2023 AMI
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.subnet_1.id
  vpc_security_group_ids      = [aws_security_group.sg_1.id]
  associate_public_ip_address = true # 퍼블릭 IP 연결 설정

  # 인스턴스에 IAM 역할 연결
  iam_instance_profile = aws_iam_instance_profile.instance_profile_1.name
  
  tags = {
    Name = "${var.prefix}-ec2-1"
  }

  # 루트 볼륨 설정
  root_block_device {
    volume_type = "gp3"
    volume_size = 32
  }

  # User data script for ec2_1
  user_data = <<-EOF
${local.ec2_user_data_base}

mkdir -p /docker_projects/tagging/source
cd /docker_projects/tagging/source
git clone https://github.com/T189216/tagging .

# 도커 이미지 생성
docker build -t tagging_1:1 .

# 생성된 이미지 실행
docker run \
    --name=tagging_1_1 \
    -p 8080:8080 \
    -v /docker_projects/tagging_1/volumes/gen:/gen \
    --restart unless-stopped \
    -e TZ=Asia/Seoul \
    -d \
    tagging_1:1
EOF
}

프로젝트 내에 해당 파일들이 있는 폴더로 이동한 후, 다음 명령어를 입력합니다.

cd terraform
terraform init

이 명령어는 Terraform이 필요로 하는 모든 설정과 플러그인을 로드하고 준비합니다.

이제 설정된 인프라 리소스대로 terraform을 실행합니다.

terraform apply

잘 실행됐습니다! 이제 AWS 콘솔로 이동해 생성된 EC2와 VPC, 인터넷 게이트웨이, 라우팅 테이블, 보안 그룹 등을 확인하면 됩니다.

AWS 콘솔에서도 확인할 수 있습니다.

인스턴스에 설정이 올바르게 적용됐는지 확인하려면 EC2 콘솔에 접속해서 확인할 수 있습니다.

EC2 콘솔에 접속하기

1. 인스턴스 연결 버튼을 클릭합니다.

2. Session Manager 탭에서 연결을 클릭합니다.

3. 해당 터미널에서 다음과 같이 입력합니다. 리눅스나 macOS에서 사용자의 권한을 루트(superuser)로 전환하는 명령어입니다.

sudo su

  • 스왑파일 확인
sudo swapon -s

  • 도커 이미지 확인
docker images

1-1. 배포된 서버 주소 확인

퍼블릭 IPv4 주소:8080에 접속하면 해당 브라우저를 확인할 수 있습니다.

2. 중단 배포하기

먼저, 프로젝트에서 변경사항을 적용하고 git에 업로드합니다.

git에 업로드할 때, 업로드하는 위치에 주의하세요!

업로드 하는 위치

업로드 하면 안되는 위치

이제 변경사항이 서버에도 적용되도록 해야겠죠?
EC2 콘솔에 접속해 다음과 같이 명령어를 입력합니다.

# 소스코드 폴더로 이동
cd /docker_projects/tagging/source
# 소스코드 최신화
git pull origin main

변경사항이 적용된 소스코드를 가져옵니다.

이 소스코드로 v2를 만들어야 합니다. 새로운 도커 이미지를 생성합시다.

# 도커 이미지 생성(v2)
docker build -t tagging_1:2 .

이제 현재 실행중인 v1을 삭제하고, v2를 실행합니다.

# 현재 실행중인 도커 컨테이너 확인
docker ps -a

# 기존에 실행된 컨테이너 제거
docker stop tagging_1_1
docker rm tagging_1_1

# 생성된 이미지 실행
docker run \
    --name=tagging_1_1 \
    -p 8080:8080 \
    -v /docker_projects/tagging_1/volumes/gen:/gen \
    --restart unless-stopped \
    -e TZ=Asia/Seoul \
    -d \
    tagging_1:2

v2가 정상적으로 실행됐습니다!

아직 도커 이미지에는 v1 이미지가 남아있는 상태입니다. 필요없는 v1 이미지는 삭제합니다.

# 기존 이미지 제거
docker rmi tagging_1:1

3. 도커 이미지 생성 자동화

현재 소스코드를 변경할 때마다 도커 이미지를 다시 생성하고 컨테이너를 제거하는 작업을 반복해야 합니다. 이 과정을 GitHub Actions을 사용해 자동화해보려고 합니다.

.github/workflows/deploy.yml

코드 변경이나 특정 파일 변경이 발생할 때 GitHub Actions를 통해 자동으로 빌드, 태깅, 릴리스, Docker 이미지 빌드 및 푸시를 수행하는 CI/CD 파이프라인을 구성하는 파일입니다.

name: 'deploy'
on:
  push:
    paths:
      - '.github/workflows/**'
      - 'src/**'
      - 'build.gradle'
      - 'Dockerfile'
      - 'readme.md'
      - 'infraScript/**'
    branches:
      - 'main'
jobs:
  makeTagAndRelease:
    runs-on: ubuntu-latest
    outputs:
      tag_name: ${{ steps.create_tag.outputs.new_tag }}
    steps:
      - uses: actions/checkout@v4
      - name: Create Tag
        id: create_tag
        uses: mathieudutour/github-tag-action@v6.1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ steps.create_tag.outputs.new_tag }}
          release_name: Release ${{ steps.create_tag.outputs.new_tag }}
          body: ${{ steps.create_tag.outputs.changelog }}
          draft: false
          prerelease: false
  buildImageAndPush:
    name: 도커 이미지 빌드와 푸시
    needs: makeTagAndRelease
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Docker Buildx 설치
        uses: docker/setup-buildx-action@v2
      - name: 레지스트리 로그인
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: set lower case owner name
        run: |
          echo "OWNER_LC=${OWNER,,}" >> ${GITHUB_ENV}
        env:
          OWNER: "${{ github.repository_owner }}"
      - name: application-secret.yml 생성
        env:
          ACTIONS_STEP_DEBUG: true
          APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET_YML }}
        run: echo "$APPLICATION_SECRET" > src/main/resources/application-secret.yml
      - name: 빌드 앤 푸시
        uses: docker/build-push-action@v3
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ env.OWNER_LC }}/tagging:${{ needs.makeTagAndRelease.outputs.tag_name }},
            ghcr.io/${{ env.OWNER_LC }}/tagging:latest

/resources/application-secret.yml

애플리케이션에서 사용되는 JWT(Json Web Token)의 비밀 키를 설정하는 파일입니다. NEED_TO_INPUT는 임의로 설정하면 됩니다.

custom:
  jwt:
    secretKey: NEED_TO_INPUT

3-1. GitHub Actions Workflow 권한 설정

이제 github 리포지터리에 접속해 해당 프로젝트의 Settings ➡️ Actinos ➡️ General 탭을 클릭하고,

GitHub Actions Workflow가 해당 리포지토리에 대해 읽기 및 쓰기 권한을 가질 수 있도록, Workflow permissions 탭에서 Read and wirte permissinos를 체크합니다.

이제, Settings ➡️ Actions secrets and variables ➡️ Actions 탭에서 New repository secret를 클릭합니다.

application-secret.yml 파일은 중요한 정보가 담긴 파일이므로, 외부에 공개되어서는 안됩니다. 해당 Workflow가 액세스 권한을 획득할 수 있도록 변수를 만들어 값을 입력해줍시다.

NEED_TO_INPUT 에는 application-secret.yml 파일에서 작성했던 secretKey 값을 입력하면 됩니다.

이제 소스코드를 github에 push합니다.

GitHub Actions이 실행되는 것을 확인할 수 있습니다.

3-2. Packages 설정

Packages에서 생성된 도커 파일 내부에 시크릿 키가 담겨있기 때문에 Packages를 비공개로 전환해야 합니다.

리포지터리에서 Packages를 클릭합니다.

리포지터리에서 Package settings를 클릭합니다.

Change package visibility 를 Private로 변경합니다.

3-3. ghcr.io 에 로그인

도커 이미지 파일에 접근 권한을 얻기 위해 로그인해야 합니다.

github 프로필 이미지를 클릭하고, Settings ➡️ Developer Settings 로 이동합니다.

Personal access tokens (classic)에서 Generate new token(classic)을 클릭해 권한을 생성합니다.

발급받은 토큰을 YOUR_TOKEN 에 적고, EC2 콘솔에서 다음 명령어를 입력합니다.

docker login ghcr.io -u USERNAME -p YOUR_TOKEN

성공적으로 로그인되었습니다!

4. 도커 이미지 교체

이제 EC2 콘솔에서 새로 생성된 도커 이미지로 교체합니다.

# 기존에 실행된 컨테이너 제거
docker stop tagging_1_1
docker rm tagging_1_1

# 생성된 이미지 실행
docker run \
    --name=tagging_1_1 \
    -p 8080:8080 \
    -v /docker_projects/tagging_1/volumes/gen:/gen \
    --restart unless-stopped \
    -e TZ=Asia/Seoul \
    -d \
    ghcr.io/t189216/tagging
    
# 기존 이미지 제거
docker rmi tagging_1:2

이제 브라우저에서 변경된 소스코드를 확인해보겠습니다.

잘 적용됩니다!

이제 소스코드를 변경하고 git에 push하면 새로운 Releases가 생성되는 것을 확인할 수 있습니다.

EC2 콘솔에서 새로 생성된 도커 이미지로 교체하면, 서버에서도 변경된 소스코드가 적용되는 것을 확인할 수 있습니다.

# 기존에 실행된 컨테이너 제거
docker stop tagging_1_1
docker rm tagging_1_1

# 새 도커 이미지 PULL
docker pull ghcr.io/t189216/tagging

# 생성된 이미지 실행
docker run \
    --name=tagging_1_1 \
    -p 8080:8080 \
    -v /docker_projects/tagging_1/volumes/gen:/gen \
    --restart unless-stopped \
    -e TZ=Asia/Seoul \
    -d \
    ghcr.io/t189216/tagging
    
# 태그가 none 인 이미지(예전에는 latest 였으니, docker pull 에 의해서 새로운 latest 태그를 가진 이미지가 생겨서, 더 이상 사용안되는 것들) 제거
docker rmi $(docker images -f "dangling=true" -q)


↗️ 도커 이미지 교체까지 자동화

profile
Today I Learned

0개의 댓글