본문 바로가기
AWS Cloud School 8기/서버가상화_클라우드 이미지

Ansible/ IaC (Infrastructure as Code)

by YUNZEE 2025. 3. 7.
728x90

📌 Ansible이란?

🔹 Ansible(앤서블)은 자동화(Automation) 도구
🔹 주로 서버 관리, 애플리케이션 배포, 네트워크 구성 자동화 등에 사용
🔹 복잡한 IT 인프라를 쉽고 빠르게 설정하고 운영할 수 있

 

📌 IaC (Infrastructure as Code)

🔹 "인프라를 코드로 관리"
🔹 서버, 네트워크, 데이터베이스 등의 IT 인프라를 코드로 작성하여 자동화하는 방식
🔹 사람이 직접 서버를 설정하는 게 아니라, 코드(YAML, JSON, Terraform 등)를 사용해 서버를 구성하고 배포

 

DevOps = IaC + CI/CD

 

IaC가 뭐냐?

"인프라를 코드로 정의하는 것을 말함"

 

- 이때 중요한 건 인프라를 코드로 정의하는 것 중에 하나로, ansible이랑 terraform을 사용한다는 것.

 

- 장점: 인프라를 On-demand로 빠르게 띄울 수 있음. 휴먼 에러를 줄일 수 있음, 그리고 동일한 환경을 재현 가능함 ( = immutable, 불변)

 

- 특징: 멱등성 성립 - 여러 번 수행해도 같은 결과가 나옴

-> 멱등성? 첫 번째 수행을 한 뒤 여러 차례 적용해도 결과를 변경시키지 않는 작업 또는 기능의 속성을 의미함. 즉, 명등한 작업의 결과는 한 번 수행하든 여러 번 수행하든 같음

 

ex) httpd 데몬이 설치되어 있으면 좋겠음 = Desired

-> 설치가 안되어 있다면 설치, 이미 설치가 되어있다면 설치를 안 할 것

 

1. ansible은 '이미 구축된' 인프라를 '관리'하는 특화

ex) virt-builder를 통해 kvm이미지를 커스터마이징 한 후, 100개의 서버를 생성한 후에 이 서버들을 한꺼번에 관리

물론 얘도 생성을 할 수 있지만 관리하는 것에 더 초점이 맞춰져 있음

 

2. terraform - '클라우드 인프라 구축'에 특화

ex) InfoGrab

 

실습) 앞으로는 계속 복사해서 사용할 예정임. VM 템플릿을 하나 만들어보자.

더보기

 

2 core 2GB 20GB, 미니멀. IP : 211.183.3.100/24

selinux , firewalld , NetworkManager 끄고 비활성화 + 레포 추가 + update

 

초기 설정

sed -i s/SELINUX=enforcing/SELINUX=disabled/g /etc/selinux/config
init 6

 

systemctl stop firewalld
systemctl disable firewalld

 

cat <<EOF> /etc/yum.repos.d/CentOS-Base.repo

[base]
name=CentOS-$releasever - Base
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os&infra=$infra
baseurl=https://vault.centos.org/7.9.2009/os/x86_64/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#released updates
[updates]
name=CentOS-$releasever - Updates
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates&infra=$infra
baseurl=https://vault.centos.org/7.9.2009/updates/x86_64/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#additional packages that may be useful
[extras]
name=CentOS-$releasever - Extras
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras&infra=$infra
baseurl=https://vault.centos.org/7.9.2009/extras/x86_64/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=centosplus&infra=$infra
baseurl=https://vault.centos.org/7.9.2009/centosplus/x86_64/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

#contrib - packages by Centos Users
[contrib]
name=CentOS-$releasever - Contrib
#mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=contrib&infra=$infra
baseurl=https://vault.centos.org/7.9.2009/contrib/x86_64/
gpgcheck=1
enabled=0
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

 

[root@cent-tem ~]# systemctl stop NetworkManager
[root@cent-tem ~]# systemctl disable NetworkManager
[root@cent-tem ~]# yum -y update

 

- Shut Down 하고

- 나머지는 넘기고 create a full clone만 선택하면 됨

- 이름은 m1으로 해주기

- 위에랑 비슷하게 m2도 만들어주기

- 그리고 추가로 m1, m2 노드에 명령을 내려 관리할 예정

- 앤서블은 server-client 구조는 아니임. 역할군이 딱 정해져 있는 게 아니라, managed node들에게 ssh로 앤서블 명령만 전달할 수 있으면 어디가 됐던 control node가 될 수 있음. 이 의미를 잘 이해하시면 좋을 것 같음.

ansible 노드 구성

더보기

- 그림에 나오는 ip로 변경해 주고 호스트 명을 밑에 명령어를 활용해서 수정해 주기

- control-node에서 각 각 명령을 내리면 ssh로 명령을 전달하면 managed node들이 m1과 m2를 관리함

- 제어를 '할' 노드

- 제어를 '당할' 노드

 

control_node 

[root@cent-tem ~]# hostnamectl set-hostname control_node

[root@cent-tem ~]# su

 

m1 - 호스트네임

[root@cent-tem ~]# hostnamectl set-hostname m1

[root@cent-tem ~]# su

 

m2 - 호스트네임

[root@cent-tem ~]#hostnamectl set-hostname m2

[root@cent-tem ~]# su

- 컨트롤노드에서 m1, m2에 통신이 되는 것을 확인할 수 있음

 

- 컨트롤 노드에 ansible 설치

[root@control_node ~]# yum install -y epel-release

[root@control_node ~]# yum install -y ansible

 

[root@control_node ~]# ansible --version

[root@control_node ~]# vi /etc/ansible/hosts

- 두대의 관리당할 서버를 적어줌.

- 위의 인벤토리에 속한 두대의 서버(m1, m2)에 ansible 명령을 전달해 보자

- 우리는 두 대를 만들었으니까 yes를 두 번 해야 됨.

- ssh 접속이 성립 안 하고 있음. 왜냐하면 키페어를 나눠갖거나, 암호를 통해 인증하지 않음.

control_node = ssh client(프라이빗키)

m1, m2 = ssh server(퍼블릭키)

[root@control_node ~]# ansible all -m ping -k

# -k : --ask-pass, 암호로 인증을 하겠다는 옵션

# 연결성을 테스트하는 모듈 : 우리가 흔히 아는 icmp 패킷을 보내는 프로토콜이 아니라 ping이라는 요청에 대해 pong을 반환하는 모듈임

yum 모듈

더보기

control_node

[root@control_node ~]# vi /etc/ansible/hosts

- 내가 사용할 수 있는 명령어들을 알려줌

 

[root@control_node ~]# ansible m1 -m yum -a "name=httpd state=present" -k

- 설치가 된 상태면 안 해줘도 됨, 근데 설치가 안된 상태면 설치해 줘라~

 

[root@control_node ~]# ansible m1 -m yum -a "name=httpd state=present"

기본 인벤토리의 [m1] 서버

-a 인자 전달

패키지 이름은 httpd이고

state는 원하는 상태 = present는 존재하는 상태를 의미함

 

absent : 존재 x (removed와 같은 의미)

installed : 설치됐으면

latest : 최신버전이었으면

present : 존재했으면

removed : 제거됐으면

- 변화가 생긴 상태

- changed가 true라고 나타난 거 보니, 변경된 게 잘 반영된 걸 알 수 있음

- 내가 원하는 상태는 state가 present 된 상태

 

m1으로 가서 확인

- httpd가 잘 설치된 상태인 걸 확인할 수 있음

control_node로 가서 확인

- 변화가 안된 상태에서는 초록색 결과창이 나타난 걸 확인할 수 있음

 - 한 번 더 입력했을 때 changed에서 변화가 안 일어난 걸 볼 수 있음. 이것을 멱등성이라고 함

- 나의 desired 부분을 입력하고 그 명령어가 어떤 프로그램을 설치한다고 했을 때, 설치가 된 프로그램이라면 설치가 안되고, 설치가 안 된 프로그램이라면 설치가 됨

- 현재 상태 확인과 바람을 대조해서 일치하지 않으면 수행을 하고, 일치하면 수행함

 

[root@control_node ~]# ansible 211.183.3.210 -m service -a "name=httpd state=started"

ansible 211.183.3.210 -m service -a "name=httpd state=started" 

systemctl = service

- 한번 더 입력하면 상태가 변경된 것을 확인할 수 있음

 

m1으로 가서 확인

- m1의 상태가 active 된 걸 확인할 수 있음

 

미니 실습) m2에 httpd라는 데몬이 잘 동작하고, 재부팅 후에도 동작할 수 있도록 ansible 명령을 수행해 보세요.

control_node

[root@control_node ~]# ansible m2 -m yum -a "name=httpd state=present" -k

[root@control_node ~]# ansible m2 -m service -a "name=httpd state=started" -k

[root@control_node ~]# ansible m2 -m service -a "name=httpd enabled=true" -k

 

m2

Ansinle collections

더보기

# 다양한 모듈들의 사용법을 알 수 있는 공식 문서

https://docs.ansible.com/ansible/latest/collections/index_module.html

# 여기에 나와있는 모듈들 중에서도 특정한 모듈만 잘 이해하면 좋음

 

실습 2) 앤서블로 m2의 방화벽을 켜보세요.

 

[root@control_node ~]# ansible m2 -m service -a "name=firewalld state=started" -k 

user 모듈

더보기

- 사용자가 관리하는 모듈

[root@control_node ~]# ansible m1 -m user -a "name=newuser1" -k

m1

[root@m1 ~]# cat /etc/passwd | grep newuser1

- m1서버에서 newuser1이라는 사용자 생성

 - m1서버에서 확인해 보면 생성이 잘 된 걸 확인할 수 있음

실습) m3 서버를 한대 추가(. 230)해서 m1, m2는 seoul 서버로 묶고 m3서버는 busan 서버로 묶어보세요.

더보기

 - m3 서버를 한대 추가하고

# 인벤토리의 서버목록은 얼마든지 중복돼도 괜찮음

 

[root@control_node ~]# vi myinven

# 이런 식으로 따로 인벤토리 파일을 생성해도 괜찮음

 

# 내가 임의로 만든 myinven이라는 인벤토리를 사용하고 싶다면 -i 옵션으로 해당 인벤토리 파일을 지정하면 됨.

 

[root@control_node ~]# ansible busan -i myinven service -a "name=firewalld state=started"

[root@control_node ~]# ansible busan -i myinven service -a "name=firewalld state=started" -k

- 다시 -k 붙이고 명령 수행

[root@control_node ~]# ansible busan -i myinven -m service -a "name=firewalld state=started" -k

# -k 붙이고 다시 명령 수행

계속 암호를 치는 게 불편하니까, ssh 인증을 암호가 아닌 키페어를 사용(public-key 방식)해서 해보자.

키페어 생성

더보기

[root@control_node ~]# ssh-keygen -b 2048 -t rsa -f ~/. ssh/id_rsa -q -N ""

- 사실상 ssh-keygen 한 것과 크게 다른 게 없음, 다만 굳이 엔터를 연속으로 치지 않고 한 번만 쳐도 키페어가 생성됨.

- ssh-keygen -b 2048 -t rsa -f ~/.ssh/id_rsa -q -N ""

-> 비트 

-> 암호화 타입

-> 파일 위치

-> 메시지 x

-> passphrase = 프라이빗 키 암호 x 

- managed 노드들에 심어줄 예정

- password: test123

- 대상 서버에 authorized_keys에 내 퍼블릭키를 등록

 

m1

[root@m1 ~]# vi ~/.ssh/authorized_keys

- m1로 가서 authorized_keys를 확인해 보자

- 퍼블릭키가 잘 등록되어 있는지 확인

 

[root@control_node ~]# ssh-copy-id -i ~/.ssh/id_rsa.pub root@211.183.3.220

[root@control_node ~]# ssh-copy-id -i ~/.ssh/id_rsa.pub root@211.183.3.230

- 나머지 두대도 퍼블릭키를 심어주자.

 

[root@control_node ~]# ansible all -i myinven -m ping

- 퍼블릭키를 심어준 다음에는 따로 암호로 인증하지 않아도 명령이 잘 전달된 걸 볼 수 있음

플레이북

= 태스크를 모아놓은 명세표(문서)

더보기

모듈: 도구

태스크: 작업

인벤토리: Managed node들의 목록

-> 모듈은 도구이고 태스크는 작업을 말함

 

모듈 = 장도리(= 망치)

태스크: 못 박기, 못 뽑기

 

1. 내가 수행하고자 하는 작업(task) = httpd라는 패키지가 설치가 됐으면 좋겠다.

=> yum이라는 도구를 사용해서 state를 present로 만들면 됨

 

2. 내가 수행하고자 하는 작업(task) = httpd라는 패키지가 없었으면 좋겠음

=> yum이라는 도구를 사용하여 state를 absent로 만들면 됨 

 

yaml 파일

- 확장자를 일반적으로 yaml, yml로 사용

- JSON파일과 호환되는 <key>:<value> 형탤로 구성된 파일

- 파이썬의 자료형 중 리스트와 딕셔너리를 통해 구성되어 있음

 

예) state: started

state = key값

started = valud값

 

리스트

동물 = ["고양이", "강아지"]

 

딕셔너리

서버 ={"cpu": 2, "ram": 2048}

 

[root@control_node ans]# vi playbook.yml

# 플레이북은 결국 다양한 태스크들을 모아놓은 파일인데, 수행할 task가 없는 플레이북을 한번 만들어보자.

 

[root@control_node ans]# ansible-playbook playbook.yml 

- 기본 인벤토리의 모든 서버한테 playbook.yml이라는 플레이북 파일을 수행

[root@control_node ans]# cp ~/myinven.

- 아까 /root 경로에 만든 인벤토리 파일을 /ans로 복사

 

[root@control_node ans]# ansible-playbook playbook.yml -i myinven

- myinven라는 인벤토리를 대상으로 playbook.yml 수행

 

[root@control_node ans]# vi playbook.yml

- 플레이북에 httpd라는 패키지를 설치하는 태스크를 추가해 보자

- 2칸 들여 쓰기를 해서 표현. 

 

[root@control_node ans]# ansible-playbook playbook.yml -i myinven

- m1, m2는 이미 설치가 되어있었기 때문에 설치 안 됐고 m3만 설치.

실습) nginx.yml이라는 플레이북을 만들어서 busan 서버에 nginx를 설치하고 동작시켜서 접속이 잘 되는 것까지 확인을 해보세요.

더보기

[root@control_node ans]# vi nginx.yml

기존 nginx.yml에 저장소 업데이트 추가

Nginx 설치
Nginx 시작 및 부팅 시 자동 실행 설정

 

[root@control_node ans]# ansible busan -i myinven -m service -a "name=firewalld state=stopped"

[root@control_node ans]# ansible busan -i myinven -m service -a "name=firewalld enabled=no"

[root@control_node ans]# ansible-playbook nginx.yml

 또 다른 풀이

 - m3에 가서 일단 nginx를 설치해 보자

# 패키지가 없다.

[root@m3 ~]# yum install -y epel-release

[root@m3 ~]# yum install -y nginx

[root@m3 ~]# systemctl restart nginx

[root@m3 ~]# yum remove httpd -y

# httpd를 제거하고 nginx를 재시작하면 잘 동작함.

 

플레이북

  httpd의 status를 absent로 하는 태스크(필요모듈=yum)

  firewalld 끄는 태스크(service모듈)

  epel-release 설치하는 태스크

  nginx 동작시키는 태스크

 

# 이런 형태로 플레이북을 구성하면 될 것 같음. (대상 = busan 서버)

[root@control_node ans]# cat nginx.yml

- name: nginx-pb
  hosts: busan
  tasks:
  - name: remove_httpd
    yum:
      name: httpd
      state: removed

  - name: firewalld_stopped
    service:
      name: firewalld
      state: stopped
      enabled: false # systemctl disable firewalld

  - name: install_epel
    yum:
      name: epel-release
      state: present

  - name: install_nginx
    yum:
      name: nginx
      state: present

  - name: start_nginx
    service:
      name: nginx
      state: started
      enabled: true

[root@control_node ans]# ansible-playbook nginx.yml -i myinven

- 접속이 잘 되는 걸 확인할 수 있음.

여러 개의 패키지를 설치하는 플레이북

더보기

[root@control_node ans]# vi multi.yml

[root@control_node ans]# ansible-playbook multi.yml -i myinven

- 경고가 뜨긴 했지만 m3서버에 (busan)에 ifconfig명령이 존재하는 걸 보면, net-tools은 잘 설치된 것으로 보임

copy 모듈

더보기

- 컨트롤 노드에 존재하는 파일을 매니지드 서버에 복사하자.

- 물론 매니지드-매니지드 복사 가능(remote_src)

- 내가 원하는 상태 = 웹서버를 설치 및 동작시키고, index.html 복사.

 

[root@control_node ans]# echo test_copy > index.html

# 넣어줄 index.html 파일 생성

# m1서버에 아까 httpd를 제거하고, nginx를 설치했기 때문에 /var/www/html 경로가 삭제됨.

centos7에 nginx를 설치하면 웹루트디렉터리가/usr/share/nginx/html

 

[root@control_node ans]# vi copy.yml

- name: copy_index_pb
  hosts: seoul
  tasks:
  - name: copy_file_task
    copy: 
      src: /ans/index.html
      dest: /var/www/html/index.html

[root@control_node ans]# ansible-playbook copy.yml -i myinven

- index.html파일이 잘 복사된 걸 확인할 수 있음.

lineinfile 모듈

더보기

[root@control_node ans]# vi lineinfile.yml

- name: lineinfile_pb
  hosts: busan
  tasks:
  - name: lineinfile
    lineinfile:
      path: /usr/share/nginx/html/index.html
      line: "line in file test"

[root@control_node ans]# ansible-playbook lineinfile.yml -i myinven

[root@m3 ~]# rm /usr/share/nginx/html/index.html

# index.html 파일을 삭제 후 create 옵션으로 파일을 생성 후 내용추가해 보자.

- 파일 생성 후 내용 추가

 file 모듈

- 파일 생성 및 권한 부여

더보기

[root@control_node ans]# vi file.yml

- name: make_file_pb
  hosts: busan
  tasks: 
  - name: make_file_task
    file:
      path: /touch-test.txt
      state: touch
      mode: '0777' # 777앞의 0은 8진수를 의미

[root@control_node ans]# ansible-playbook file.yml -i myinven 

m3

- 최상위 디렉터리에 파일 생성 및 권한부여가 잘 된 걸 확인할 수 있음.

실습) rapa.inven이라는 인벤토리파일을 하나 만든 후 [web] 목록을 하나 만든다, [web]에 속하는 서버는 IP가 211.183.3.150,211.183.3.160 인 서버이며 여기에 간단한 index.html 파일을 넣어서 배포하고자 하는데 배포할 파일은 웹상의 https://www.w3.org/TR/PNG/iso_8859-1.txt 이 파일을 index.html로 다운로드하여서 배포하면 된다. wget 같은 모듈을 찾아서 한번 해보세요. 노드에 따로 파일을 다운받아서 넣지 말라는 뜻.

더보기

1. 이때 IP가 211.183.3.150, 211.183.3.160 인 서버

web1 - 211.183.3.150

web2 - 211.183.3.160

2.rapa.inven이라는 인벤토리파일에 [web] 목록 생성

3. 키 설정해 주기

ssh-copy-id -i ~/.ssh/id_rsa.pub root@211.183.3.150

ssh-copy-id -i ~/.ssh/id_rsa.pub root@211.183.3.160

 

4.  여기에 간단한 index.html 파일을 넣어주고, 웹 상의https://www.w3.org/TR/PNG/iso_8859-1.txt이 파일을 받아오기.

- name: deploy_index_pb
  hosts: web
  tasks:
  - name: firewalld_stopped
    service:
      name: firewalld
      state: stopped
      enabled: false # systemctl disable firewalld

  - name: install_epel
    yum:
      name: epel-release
      state: present

  - name: install_nginx
    yum:
      name: nginx
      state: present

  - name: start_nginx
    service:
      name: nginx
      state: started
      enabled: true

  - name: download_index_task
    get_url:
      url: https://www.w3.org/TR/PNG/iso_8859-1.txt
      dest: /usr/share/nginx/html/index.html
      force: true #파일이 존재하면 덮어쓰기

잘 뜨는 걸 확인할 수 있음.

shell 모듈

- 명령을 수행하는 shell 모듈의 경우, 단순히 명령을 수행하기  때문에 멱등성이 보장되지 않는다.

더보기

[root@control_node ans]# vi shell.yml

- name: shell_test_pb
  hosts: web
  tasks:
  - name: shell_test_task
    shell: "{{ item }}"
    with_items:
    - "mkdir /shelltest"
    - "cp /root/anaconda-ks.cfg /shelltest"
    - "ls -al /shelltest"

[root@control_node ans]# ansible-playbook shell.yml -i rapa.inven

# 플레이북 수행 후 

- 이미 파일이 있으니까... 이 명령어를 수행하,,,,결론 shell이란 모듈은 사용 안 하는 게 좋음

- 만약에 파일이 shelltest에 생성이 되지 않는다면, 한 번 지우고 다시 ansible을 실행시키면 됨

실습 1) 인벤토리의 [web] 서버들을 대상으로 하여 프리템플릿을 배포해 보세요.

더보기

 플레이북을 만들어 실행시키기 전에 이 과정이 수동으로 어떤 식으로 실행되는 건지 해본다면?

[root@control_node ans]# yum install -y wget

[root@control_node ans]# wget https://www.free-css.com/assets/files/free-css-templates/download/page293/dgital.zip

플레이북을 사용해서 구현한다면?

[root@control_node ans]# vi html.yml 

- name: Deploy DGital Web Template
  hosts: web
  become: true
  tasks:
    - name: Install required packages (Nginx, Unzip)
      yum:
        name:
          - nginx
          - unzip
        state: present

    - name: Download DGital template zip file
      get_url:
        url: "https://www.free-css.com/assets/files/free-css-templates/download/page293/dgital.zip"
        dest: "/tmp/dgital.zip"
        mode: '0644'

    - name: Extract template to Nginx web directory
      unarchive:
        src: "/tmp/dgital.zip"
        dest: "/usr/share/nginx/html/" #여기 뒤에 unzip한 상태로 오기 때문에 http://211.183.3.150/digital-agency-html-template/ 이 루트를 입력해줘야 웹페이지가 정상적으로 보임
        remote_src: yes

    - name: Ensure Nginx is running
      service:
        name: nginx
        state: started
        enabled: true

 [root@control_node ans]# ansible-playbook html.yml -i rapa.inven 

실습 2) 앤서블 플레이북을 통해 m3는 nfs-server로, m1은 nfs-client로 구성해 보세요.

더보기

[root@control_node ans]# vi nfs.yml

- name: setup NFS
  hosts: busan
  tasks:
  - name: install_nfs
    yum:
      name: nfs-utils
      state: present

  - name: start_nfs
    service:
      name: nfs-server
      state: started

  - name: create_dir
    file:
      path: /shared
      state: directory
      mode: '0777'

  - name: create_file
    lineinfile:
      path: /etc/exports
      line: "/shared 211.183.3.*(rw)"
      create: true

  - name: exportfs
    shell: exportfs -r
    become: true

  - name: restart_nfs_server
    service:
      name: nfs-server
      state: restarted
      enabled: yes

- name: setup NFS_client
  hosts: 211.183.3.210
  tasks:
  - name: yum_nfs_client
    yum:
      name: nfs-utils
      state: present

  - name: create_dir_clinet
    file:
      path: /remote
      state: directory
      mode: '0777'

  - name: mount
    mount:
      path: /remote
      src: "211.183.3.230:/shared"
      fstype: nfs
      opts: defaults
      state: mounted

[root@control_node ans]# ansible-playbook nfs.yml -i myinven 

 

 m1

m3

마운트가 잘 된 걸 확인할 수 있음.

728x90