공부하기싫어
article thumbnail

CI/CD 환경 구축을 끝내서 최종 정리하려고 함

이번 포스팅으로 인프라 세팅은 끝내고 이제 다시 front-end 와 back-end 작업에 들어갈 듯 하다

 

웹서비스 인프라 설계 및 구축

글은 각 제목 아래 설계했던 내용 - 구축한 내용 으로 작성하였음

목차

1. Infrastructure Diagram

2. AWS resources

  - subnet, SG, EIP, 

3. jenkins-server

  - install & config docker

  - create AMI

4. db-server

5. web-server

6. CI/CD pipeline

  - github + hub.docker

  - jenkins config

  - ansible

  - Webhook

  - Jenkinsfile

  - playbook.yaml

7. Route53

 

 

1. Infrastructure Diagram

시스템 설계도

- github 와 hub.docker 를 이용한 ci/cd 파이프라인을 구성

- S3 버킷은 크롤링한 이미지를 저장하고 s3 url 을 메타데이터로 저장해서 불러오도록 했다.

- 최근 배웠던 k8s 를 적용하기엔 규모가 작기때문에 ansible 로 원격지(web-server) 를 제어하기로 했다

- db-server 는 프라이빗 서브넷에 배치해서 보안성을 높였다 (사실 별로 든게 없긴 하다)

 

2. AWS resources

인스턴스 환경

  Jenkins server Web server DB server
OS ubuntu 22.04 ubuntu 22.04 ubuntu 22.04
인스턴스 유형 t3a.medium t3a.small t2.micro
key pair dev-all prod-blackpink dev-all
VPC default default default
subnet 2a 2a 2a
public ip EIP yes yes
SG-inbound 22,8080 22,80,443, 8080 22,3306
SG-outbound anywhere anywhere anywhere
storage 16GiB 16GiB 30GiB
container jenkins php-apache, python mysql

 

리소스 태그

  Jenkins server Web server DB server private subnet S3 bucket
tag - Name ec2-jenkins_server-01 ec2-web_server_01 ec2-db_server_01 subnet-ap_northeast_2a-private_01
s3bucket-ap_northeast_2-public_image_storage
tag - Project General ResponsiveWeb-blackpinkinyourarea General General ResponsiveWeb-blackpinkinyourarea
tag - Environment Develop Production General Production Production

 

보안그룹 태그

  SG-ssh SG-for-jenkins_server SG-for-web_server
SG-for-db_server
tag - Name sg-default_vpc-ec2-ssh sg-default_vpc-ec2-jenkins sg-default_vpc-ec2-webserver
sg-default_vpc-ec2-dbserver
tag - Project General General ResponsiveWeb-blackpinkinyourarea General
tag - Environment General Develop Production General

- jenkins server 는 앞으로도 cicd 환경을 구축할때 사용하고 또 빌드가 실행될 곳이기 때문에 medium 에 EIP 를 할당해주려고 한다

- db server 의 스토리지는 프리티어 최대 사용량인 30GiB 로 설정해줬다

- 인스턴스의 들의 보안그룹은 SG-ssh, SG-for-jenkins_server, SG-for-web_server, SG-for-db_server 이렇게 4개 사용할 예정이다

- S3 bucket 은 사진 다운로드를 위해 퍼블릭 액세스 오픈해서 사용한다

 

subnet

먼저 서브넷을 만들어줬다

private subnet

CIDR 는 기본 vpc 에서 남은 대역중 다음 대역을 선택해줬다

 

EIP

eip 연결

 

그리고 위 설계에 맞춰 jenkins server 를 만들어주고

ubuntu 22.04 이미지를 선택해 준다음

EIP 를 연결해줬다

 

EIP 태그

Name : eip-ap_northeast_2-conn_jenkins

Project : General

Environment : Develop

 

Name : eip-ap_northeast_2-conn_blackpink

Project : ResponsiveWeb-blackpinkinyourarea

Environment : Production

 

 

3. jenkins-server

docker 설치

업데이트 및 필요 패키지 설치

sudo apt update
sudo apt-get update

sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

Docker GPG key 추가

sudo mkdir -p /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

 

리포지토리 설정

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

 

Docker 엔진 설치

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io -y

 

Docker daemon 설정

cat <<EOF | sudo tee /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF

sudo systemctl enable --now docker

 

  • CRI container runtime 설치

cri-docker 패키지 다운로드

https://github.com/Mirantis/cri-dockerd/releases/tag/v0.2.6

 

cri-docker 패키지 설치

sudo apt install ./<해당버전>.deb

 

cri-docker 패키지 설치 후

sudo systemctl enable cri-docker.service
sudo systemctl enable --now cri-docker.socket

 

sudo 없이 docker 명령어 실행

그룹 추가
sudo groupadd docker

그룹에 사용자 추가
sudo usermod -aG docker {user_id}

docker 재시작
sudo service docker restart

권한오류 Got permission denied while trying to ...
sudo chown root:docker /var/run/docker.sock
혹은
sudo chmod 666 /var/run/docker.sock

 

AMI

ami

ami tag

Name : ami-docker_in_ubuntu

Project : General

Environment : General

 

인스턴스 생성

instances
docker

sudo 없이 docker 명령이 잘 실행 되는 모습

 

 

4. db-server

인스턴스 환경

  Jenkins server Web server DB server
OS ubuntu 22.04 ubuntu 22.04 ubuntu 22.04
인스턴스 유형 t3a.medium t3a.small t2.micro
key pair dev-all prod-blackpink dev-all
VPC default default default
subnet 2a 2a 2a
public ip EIP yes yes
SG-inbound 22,8080 22,80,443, 8080 22,3306
SG-outbound anywhere anywhere anywhere
storage 16GiB 16GiB 30GiB
container jenkins php-apache, python mysql

 

리소스 태그

  Jenkins server Web server DB server private subnet S3 bucket
tag - Name ec2-jenkins_server-01 ec2-web_server_01 ec2-db_server_01 subnet-ap_northeast_2a-private_01
s3bucket-ap_northeast_2-public_image_storage
tag - Project General ResponsiveWeb-blackpinkinyourarea General General ResponsiveWeb-blackpinkinyourarea
tag - Environment Develop Production General Production Production

 

위 설계에 맞춰서 인스턴스 생성

 

private subnet 에 위치한 db server 에 mysql container 환경설정

먼저 개발 환경에서 docker image 를 tar 로 압축해서 보내줌

docker save

그리고 private subnet 에 위치한 ec2 에 ssh 접속해서

docker load 로 image 에 저장해줌

docker load

이후 mysql container 생성해줌

ubuntu@ip-172-31-78-182:~$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
ubuntu@ip-172-31-78-182:~$ 
ubuntu@ip-172-31-78-182:~$ docker run -it --name mysql-db -e MYSQL_ROOT_PASSWORD=비밀번호 -d -p 3306:3306 mysql:latest
f30f097dd22fdd38581b9e7c9cc44991a5440423d6b6262fa4fe9408569e7810
ubuntu@ip-172-31-78-182:~$ 
ubuntu@ip-172-31-78-182:~$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                                  NAMES
f30f097dd22f   mysql:latest   "docker-entrypoint.s…"   7 seconds ago   Up 4 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql-db
ubuntu@ip-172-31-78-182:~$ 
ubuntu@ip-172-31-78-182:~$

 

db 랑 테이블을 만들어보자

DB : blackpink
table : test table : blackpink_img_data
column type column type
sample_no int DB_MEM_NAME varchar(10)
sample_name varchar(100) DB_YEAR int
    DB_MONTH int
    DB_DAY int
    DB_FILENUM int
    DB_S3URL varchar(300)
    DB_DOWNLOAD int

 

위처럼 간단하게 2개 테이블만 만들어보자

 

create table

create table test (
	sample_no int,
	sample_name varchar(100)
);

create table blackpink_img_data (
	DB_MEM_NAME varchar(10),
	DB_YEAR int,
	DB_MONTH int,
	DB_DAY int,
	DB_FILENUM int,
	DB_S3URL varchar(300),
	DB_DOWNLOAD int
);

 

테스트 데이터 넣어보고 확인하기

 

test

 

액셀 백업 파일 안의 데이터 db로 옮기기

이전에 액셀파일로 백업해놨던 데이터를

private subnet 의 mysql container 안으로 옮겨보자

 

private subnet 에 위치한 db server 로 mysql connect 해야하기 때문에 

기존 개발 환경인 instance 에서 접속해볼 예정이다.

import pandas as pd
import pymysql

con = pymysql.connect(host='172.31.78.182', user='root', password='비밀번호',
                       db='blackpink', charset='utf8') # 한글처리 (charset = 'utf8')

cur = con.cursor()

excel_sheet = pd.read_excel('/home/ubuntu/db-backup.xlsx', 
                    sheet_name = 'backup-data', engine='openpyxl')

sql_insert_1 = 'insert into blackpink_img_data values(%s, %s, %s, %s, %s, %s, %s)'
for idx in range(len(excel_sheet)):
    	cur.execute(sql_insert_1, tuple(excel_sheet.values[idx]))
con.commit()

con.close()

db server 와 같은 vpc 에 위치한 dev server 에서 실행할 내용

 

ubuntu@blackpinkinyourarea:~$ python3 migration_backup_data.py 
ubuntu@blackpinkinyourarea:~$

실행

 

select * from blackpink_img_data;

2170 개 데이터 모두 잘 옮겨진 모습

 

5. web-server

인스턴스 환경

  Jenkins server Web server DB server
OS ubuntu 22.04 ubuntu 22.04 ubuntu 22.04
인스턴스 유형 t3a.medium t3a.small t2.micro
key pair dev-all prod-blackpink dev-all
VPC default default default
subnet 2a 2a 2a
public ip EIP yes yes
SG-inbound 22,8080 22,80,443, 8080 22,3306
SG-outbound anywhere anywhere anywhere
storage 16GiB 16GiB 30GiB
container jenkins php-apache, python mysql

 

리소스 태그

  Jenkins server Web server DB server private subnet S3 bucket
tag - Name ec2-jenkins_server-01 ec2-web_server_01 ec2-db_server_01 subnet-ap_northeast_2a-private_01
s3bucket-ap_northeast_2-public_image_storage
tag - Project General ResponsiveWeb-blackpinkinyourarea General General ResponsiveWeb-blackpinkinyourarea
tag - Environment Develop Production General Production Production

위 설계에 맞춰 인스턴스 생성

 

6. CI/CD pipeline

git repository 만들기

git repo

 

일단 jenkins 서버 환경 설정 해 놓고

로컬에 클론시킬 예정

 

hub.docker repository 생성

hub.docker repo

jenkins 에 연결할 hub.docker repo

 

 

Jenkins 설치

sudo docker run -itd --name jenkins -p 8080:8080 -p 50000:50000 -v /docker/jenkins:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -e TZ=Asia/Seoul -u root jenkins/jenkins:latest

 

Jenkins container에 docker 설치

docker exec -it 젠킨스컨테이너이름 bash

curl -fsSL get.docker.com -o get-docker.sh 
sh get-docker.sh

 

Jenkins 초기 비밀번호

젠킨스 컨테이너 안에서
cat /var/jenkins_home/secrets/initialAdminPassword

 

권장 플러그인 설치

 

자격 증명 생성

 

Github 자격증명 생성

kind : Username with Password

Username : Github 계정명

Password : github persnal access token

ID : 젠킨스 내에서 자격증명을 가리키기 위해 사용할 변수 이름

 

Docker Hub 자격증명 생성

kind : Username with Password

Username : Docker Hub 계정명

Password : Docker Hub persnal access token ( hub.docker - account settings - security - new access tokens )

ID : 젠킨스 내에서 자격증명을 가리키기 위해 사용할 변수 이름

 

Webhook

git repo webhook
jenkins config

hook trigger for GITScm polling

Jenkinsfile

pipeline {
  agent any

  // 해당 스크립트 내에서 사용할 로컬 변수들 설정-
  // 레포지토리가 없으면 생성됨
  // Credential들에는 젠킨스 크레덴셜에서 설정한 ID를 사용
  environment {
    dockerhubRegistry = 'cyaninn/prod-web-blackpinkinyourarea'
    dockerhubRegistryCredentail = 'cred-dockerhub'
    
    githubCredential = 'cred-github'
    gitEmail = 'sounddevice3@gmail.com'
    gitName = 'cyaninn-entj'
  }

  stages {

    // 깃허브 계정으로 레포지토리를 클론한다.
    stage('Checkout Application Git Branch') {
      steps {
        checkout([$class: 'GitSCM', branches: [[name: '*/main']], extensions: [], \
        userRemoteConfigs: [[credentialsId: githubCredential, url: 'https://github.com/cyaninn-entj/prod-web-blackpinkinyourarea.git']]])
        sh "pwd"
        sh "ls"
      }
      // steps 가 끝날 경우 실행한다.
      // steps 가 실패할 경우에는 failure 를 실행하고 성공할 경우에는 success 를 실행한다.
      post {
        failure {
          echo 'Repository clone failure' 
        }
        success {
          echo 'Repository clone success' 
        }
      }
    }

    //python-crawling-server build
    stage('backend - Docker Image Build') {
      steps {
        sh "docker build ./back-end -t ${dockerhubRegistry}:backend.${currentBuild.number}"
        sh "docker build ./back-end -t ${dockerhubRegistry}:backend.latest"
      }
      // 성공, 실패 시 슬랙에 알람오도록 설정
      post {
        failure {
          echo 'Docker image build failure'
          //slackSend (color: '#FF0000', message: "FAILED: Docker Image Build '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
        success {
          echo 'Docker image build success'
          //slackSend (color: '#0AC9FF', message: "SUCCESS: Docker Image Build '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
      }
    }  

    //python-crawling-server push
    stage('backend - Docker Image Push') {
      steps {
        withDockerRegistry([credentialsId: "${dockerhubRegistryCredentail}", url:""]) {
          sh "docker push ${dockerhubRegistry}:backend.${currentBuild.number}"
          sh "docker push ${dockerhubRegistry}:backend.latest"
          // 10초 쉰 후에 다음 작업 이어나가도록 함
          sleep 10
        } 
      }
      post {
        failure {
          echo 'Docker Image Push failure'
          sh "docker rmi ${dockerhubRegistry}:backend.${currentBuild.number}"
          sh "docker rmi ${dockerhubRegistry}:backend.latest"
          //slackSend (color: '#FF0000', message: "FAILED: Docker Image Push '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
        success {
          echo 'Docker Image Push success'
          sh "docker rmi ${dockerhubRegistry}:backend.${currentBuild.number}"
          sh "docker rmi ${dockerhubRegistry}:backend.latest"
          //slackSend (color: '#0AC9FF', message: "SUCCESS: Docker Image Push '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
      }
    }



    //php-apache-server build
    stage('frontend - Docker Image Build') {
      steps {
        // 도커 이미지 빌드
        sh "docker build ./front-end -t ${dockerhubRegistry}:frontend.${currentBuild.number}"
        sh "docker build ./front-end -t ${dockerhubRegistry}:frontend.latest"
      }
      // 성공, 실패 시 슬랙에 알람오도록 설정
      post {
        failure {
          echo 'Docker image build failure'
          //slackSend (color: '#FF0000', message: "FAILED: Docker Image Build '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
        success {
          echo 'Docker image build success'
          //slackSend (color: '#0AC9FF', message: "SUCCESS: Docker Image Build '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
      }
    }  

    //php-apache-server push
    stage('frontend - Docker Image Push') {
      steps {
        // 젠킨스에 등록한 계정으로 ECR 에 이미지 푸시
        withDockerRegistry([credentialsId: "${dockerhubRegistryCredentail}", url:""]) {
          sh "docker push ${dockerhubRegistry}:frontend.${currentBuild.number}"
          sh "docker push ${dockerhubRegistry}:frontend.latest"
          // 10초 쉰 후에 다음 작업 이어나가도록 함
          sleep 10
        } 
      }
      post {
        failure {
          echo 'Docker Image Push failure'
          sh "docker rmi ${dockerhubRegistry}:frontend.${currentBuild.number}"
          sh "docker rmi ${dockerhubRegistry}:frontend.latest"
          //slackSend (color: '#FF0000', message: "FAILED: Docker Image Push '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
        success {
          echo 'Docker Image Push success'
          sh "docker rmi ${dockerhubRegistry}:frontend.${currentBuild.number}"
          sh "docker rmi ${dockerhubRegistry}:frontend.latest"
          //slackSend (color: '#0AC9FF', message: "SUCCESS: Docker Image Push '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
        }
      }
    }

    // updated docker image 태그를 git push 
    stage('Deploy') { 
      steps {
        //sh "ansible prod-web-blackpink -m ping -i Inventory.ini"
        sh "ansible-playbook playbook.yaml -i Inventory.ini"
      } /*
      post {
          failure {
            echo 'K8S Manifest Update failure'
            slackSend (color: '#FF0000', message: "FAILED: K8S Manifest Update '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
          }
          success {
            echo 'K8s Manifest Update success'
            slackSend (color: '#0AC9FF', message: "SUCCESS: K8S Manifest Update '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
          }
      } */
    }  
  }
}

이전 쿠버네티스 프로젝트에서 적용했었던 파일을 조금 수정했다.

2개의 컨테이너를 빌드하고 hub.docker 로 푸시하도록 했고 배포는 ansible playbook 을 사용하게끔 했다.

 

ansible 연결

ansible 설치

sudo apt-get install software-properties-common
sudo apt-add-repository ppa:ansible/ansible
sudo apt-get update
sudo apt-get install ansible

ansible -h

jenkins-server 에 설치해줬다

 

inventory 등록

private ip 주소로 등록해주려고 한다

기본 inventory 파일 경로 - /etc/ansible/hosts

나는 새로 만들어서 사용할꺼다

#Inventory.ini

[prod]
web-blackpink ansible_host=172.31.14.71

[prod:vars]
ansible_connection=ssh
ansible_port=22
ansible_user=ubuntu 
ansible_private_key_file=/home/ubuntu/keypairs/prod-blackpink.pem

dir

 

ping test

prod-ansible 디렉토리로 이동해서

ubuntu@jenkins-server:~/prod-ansible$ ansible prod -m ping -i Inventory.ini
[DEPRECATION WARNING]: COMMAND_WARNINGS option, the command warnings feature is being removed. This
 feature will be removed from ansible-core in version 2.14. Deprecation warnings can be disabled by
 setting deprecation_warnings=False in ansible.cfg.
web-blackpink | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
ubuntu@jenkins-server:~/prod-ansible$

이렇게 실행해주면 ping 결과가 잘 확인되었다

 

playbook.yaml

---
- name: start docker container
  hosts: prod-web-blackpink
  remote_user: root
  gather_facts: false

  tasks:
  - name: Stop old-version web-server container
    community.docker.docker_container:
      name: web-server
      state: stopped
  - name: Stop old-version crawling-server container
    community.docker.docker_container:
      name: crawling-server
      state: stopped

  - name: remove old-version web-server container
    community.docker.docker_container:
      name: web-server
      state: absent
  - name: remove old-version crawling-server container
    community.docker.docker_container:
      name: crawling-server
      state: absent
  
  - name: remove image frontend.latest
    command: "docker image rm cyaninn/prod-web-blackpinkinyourarea:frontend.latest"
  - name: remove image backend.latest
    command: "docker image rm cyaninn/prod-web-blackpinkinyourarea:backend.latest"

  - name: running web-server
    command: "docker run -d -it --name web-server -p 80:80 cyaninn/prod-web-blackpinkinyourarea:frontend.latest"
  - name: running python-server
    command: "docker run -d -it --name crawling-server cyaninn/prod-web-blackpinkinyourarea:backend.latest"

playbook은 2개의 컨테이너를 각각 멈춘 후 삭제하고

이전 버전의 latest 이미지를 삭제해주고

다시 최신 버전의 컨테이너 이미지로 docker run 하게 해줬다.

(latest 라는 같은 태그로 계속 남아있어서 업데이트가 되지 않았었따..)

7. Route53

ASDF