본문 바로가기
[Microsoft] Cyber Security School 7기

Azure 클라우드 + cloud-init 과제

by juyeon2110 2026. 5. 17.

Azure 클라우드 + cloud-init 을 이용하여 초급 실습 예제 1개, 중급 실습 예제 1개, 고급 실습 예제 1개 만들기

cloud-init : 클라우드 인스턴스(VM)가 처음 부팅될 때 호스트 이름 설정, 네트워크 구성, SSH 키 생성, 패키지 설치 등 초기화 작업을 자동화하는 업계 표준 오픈소스 도구. AWS, Azure, Google Cloud 등 주요 클라우드 환경에서 OS 이미지(AMI 등)의 공통 설정을 동적으로 변경하여 사용함.

VM 생성
부팅 시작
cloud-init 시작
① 네트워크 설정
② 패키지 업데이트/설치 ← packages
③ 파일 생성 ← write_files
④ 명령어 실행 ← runcmd
완료 (cloud-init status: done)
서비스 정상 운영

 

 

리소스 그룹 생성

cloud-init 파일 작성

#cloud-config.yaml
 
 
package_update: true
package_upgrade: false
 
 
 packages:
        - nginx
        - curl
 
users:
        - name: azureuser
          groups: sudo
          shell: /bin/bash
          sudo: [ 'ALL=(ALL) NOPASSWD:ALL' ]
 
write_files:
        - path: /var/www/html/index.html
          content: |
              <html>
                  <body>
                      <h1>Azure + cloud-init 실습 성공!</h1>
                  </body>
              </html>
          owner: www-data:www-data
          permissions: '0644'
 
runcmd:
         - systemctl enable nginx
         - systemctl start nginx
         - echo "cloud-init 완료" >> /var/log/mysetup.log
 
EOF

VM 생성. Public IP는 4.230.24.123

포트 80 오픈

#SSH 접속
 
ssh azureuser@4.230.24.123

cloud-init이 정상 실행됐는지 확인

Public ip로 브라우저 접속

LAMP 스택 : 웹 애플리케이션 구축 및 배포에 널리 사용되는 오픈소스 소프트웨어 스택. Linux(운영체제), Apache(HTTP server), MySQL(데이터베이스) 및 PHP, Perl 또는 Python(프로그래밍 언어)의 약자.

리소스 그룹 생성

#cloud-config-lamp.yaml
 
 
package_update: true
 
#packages — apache2, mysql-server, php, php-mysql, libapache2-mod-php 설치
 
packages:
    - apache2
    - mysql-server
    - php
    - php-mysql
    - libapache2-mod-php
 
 
#write_files — DB 초기화 SQL 파일(/tmp/init_db.sql)PHP 페이지(/var/www/html/index.php) 생성
 
 
write_files:
    - path: /tmp/init_db.sql
      permissions: '0600'
      content: | CREATE DATABASE IF NOT EXISTS appdb CHARACTER SET utf8mb4;
           CREATE USER IF NOT EXISTS 'appuser'@'localhost' IDENTIFIED BY 'SecurePass123!';
           GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
           USE appdb;
           CREATE TABLE IF NOT EXISTS users (
               id INT AUTO_INCREMENT PRIMARY KEY,
               username VARCHAR(50) NOT NULL,
               created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );
           INSERT INTO users (username) VALUES ('admin'), ('testuser');
           FLUSH PRIVILEGES;
 
 
    - path: /var/www/html/index.php
      owner: www-data:www-data
      permissions: '0644'
      content: |
           <?php $conn = new mysqli('localhost', 'appuser', 'SecurePass123!', 'appdb');
           if ($conn->connect_error) {
                die("DB 연결 실패: " . $conn->connect_error);
           }
          $res = $conn->query("SELECT * FROM users");
          echo "<h2>DB Connection OK!</h2><ul>";
          while($row = $res->fetch_assoc()) {
              echo "<li>ID: " . $row['id'] . " / User: " . $row['username'] . " / Created: " . $row['created_at'] . "</li>";
           }
          echo "</ul>";
          ?>
 
#runcmd — MySQL/Apache 시작, SQL 실행, 보안을 위해 SQL 파일 삭제, Apache 재시작
runcmd:
    - systemctl enable mysql apache2
    - systemctl start mysql
    - mysql -u root < /tmp/init_db.sql - rm -f /tmp/init_db.sql
    - a2enmod rewrite - systemctl restart apache2
    - echo "LAMP done" >> /var/log/lamp.log
EOF

VM 생성, Public IP는 52.231.24.173

#SSH 접속
 
ssh azureuser@52.231.24.173

SSH 접속 및 검증

브라우저로 접속. (트러블슈팅 중 public ip 변경)

phpMyAdmin 설치 (GUI DB)

Kubernetes 노드 클러스터 : 컨테이너화된 애플리케이션을 실행하기 위해 그룹화된 노드(물리적/가상 머신)의 집합. 클러스터는 클러스터를 관리하는 마스터 노드(Control Plane)와 실제 애플리케이션 컨테이너가 배포되어 실행되는 워커 노드(Worker Node)로 구성됨.

VNet(Azure Virtual Network) : Azure 클라우드 내 독립된 네트워크. 프라이빗 네트워크의 기본 구성 요소

NSG(Network Security Group, 네트워크 보안 그룹) : 클라우드(예: Microsoft Azure 네트워크 보안 그룹) 환경에서 가상 네트워크 서브넷 및 네트워크 인터페이스의 트래픽을 필터링하는 방화벽 서비스

리소스 그룹, VNet 생성

#/home/nova/control-plane.yaml
#Control Plane용 cloud-init 파일
 
write_files:
    - path: /etc/modules-load.d/k8s.conf
      content: |
          overlay
          br_netfilter
    - path: /etc/sysctl.d/k8s.conf
      content: |
          net.bridge.bridge-nf-call-iptables = 1
          net.bridge.bridge-nf-call-ip6tables = 1
          net.ipv4.ip_forward = 1
    - path: /tmp/install-k8s.sh
      permissions: '0755'
      content: |
          #!/bin/bash
          set -e
          # containerd install
          apt-get update -q
          apt-get install -y ca-certificates curl gnupg apt-transport-https
          install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | 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" > /etc/apt/sources.list.d/docker.list apt-get update -q apt-get install -y containerd.io mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml systemctl enable containerd && systemctl restart containerd # kubeadm kubelet kubectl install curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /" > /etc/apt/sources.list.d/kubernetes.list apt-get update -q apt-get install -y kubelet kubeadm kubectl apt-mark hold kubelet kubeadm kubectl systemctl enable kubelet # kubeadm init kubeadm init \ --pod-network-cidr=10.244.0.0/16 \ --apiserver-advertise-address=$(hostname -I | awk '{print $1}') \ --cri-socket unix:///run/containerd/containerd.sock # kubeconfig mkdir -p /home/azureuser/.kube cp /etc/kubernetes/admin.conf /home/azureuser/.kube/config chown -R azureuser:azureuser /home/azureuser/.kube # Flannel CNI sudo -u azureuser kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml # save join command kubeadm token create --print-join-command > /tmp/join-command.sh chmod 644 /tmp/join-command.sh echo "CONTROL_PLANE_READY" > /tmp/k8s-status runcmd: - modprobe overlay && modprobe br_netfilter - sysctl --system - bash /tmp/install-k8s.sh >> /var/log/k8s-install.log 2>&1 EOF
#/home/nova/worker.yaml #Worker 노드용 cloud-init 파일 write_files: - path: /etc/modules-load.d/k8s.conf content: | overlay br_netfilter - path: /etc/sysctl.d/k8s.conf content: | net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 - path: /tmp/install-k8s.sh permissions: '0755' content: | #!/bin/bash set -e # containerd install apt-get update -q apt-get install -y ca-certificates curl gnupg apt-transport-https install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | 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" > /etc/apt/sources.list.d/docker.list apt-get update -q apt-get install -y containerd.io mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml systemctl enable containerd && systemctl restart containerd # kubeadm kubelet kubectl install curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /" > /etc/apt/sources.list.d/kubernetes.list apt-get update -q apt-get install -y kubelet kubeadm kubectl apt-mark hold kubelet kubeadm kubectl systemctl enable kubelet - path: /tmp/join-worker.sh permissions: '0755' content: | #!/bin/bash CTRL_IP="10.0.0.4" MAX=30 COUNT=0 while [ $COUNT -lt $MAX ]; do CMD=$(ssh -o StrictHostKeyChecking=no \ -o ConnectTimeout=5 \ azureuser@$CTRL_IP \ "cat /tmp/join-command.sh" 2>/dev/null) if [ -n "$CMD" ]; then bash -c "$CMD" echo "JOIN_SUCCESS" > /tmp/k8s-status break fi sleep 20 COUNT=$((COUNT+1)) done runcmd: - modprobe overlay && modprobe br_netfilter - sysctl --system - bash /tmp/install-k8s.sh >> /var/log/k8s-install.log 2>&1 - bash /tmp/join-worker.sh >> /var/log/k8s-join.log 2>&1 EOF

Control Plane VM (Public IP: 20.196.209.206, Private IP: 10.0.0.4) , Worker VM (Public IP: 20.249.10.33, Private IP: 10.0.0.5) 생성

 

#Control Plane에 SSH 접속
 
ssh -i /root/.ssh/id_rsa azureuser@20.196.209.206
#클러스터 상태 확인
kubectl get nodes
#결과
 
azureuser@vm-k8s-control:~$ kubectl get nodes
 
NAME                 STATUS   ROLES             AGE       VERSION
vm-k8s-control    Ready      control-plane    17m        v1.29.15
#Worker에 SSH접속
ssh -i /root/.ssh/id_rsa azureuser@20.249.10.33
 
#Kubernetes 클러스터에 worker 노드를 추가
sudo kubeadm join 10.0.0.4:6443 --token jo7l8l.1ffwmquvzz7exeuy --discovery-token-ca-cert-hash sha256:7d4523acd7abd81fdff38e150a0c4937b3d3b40011f5e698fc3553f06c6359d3
#결과
 
Last login: Wed May 13 01:21:45 2026 from 61.108.60.26 azureuser@vm-k8s-worker1:~$ sudo kubeadm join 10.0.0.4:6443 --token jo7l8l.1ffwmquvzz7exeuy --discovery-token-ca-cert-hash sha256:7d4523acd7abd81fdff38e150a0c4937b3d3b40011f5e698fc3553f06c6359d3 [preflight] Running pre-flight checks
 
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet [kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
#Control Plane에서 클러스터 상태 확인
kubectl get nodes
#결과.
Kubernetes 클러스터 완성
 
azureuser@vm-k8s-control:~$ kubectl get nodes
NAME                  STATUS      ROLES                AGE      VERSION
vm-k8s-control     Ready         control-plane       21m       v1.29.15
vm-k8s-worker1   Ready         <none>                44s        v1.29.15
# nginx Pod 배포 및 외부 접속
 
# Pod 배포
kubectl run nginx --image=nginx --port=80
 
# NodePort 서비스 생성
kubectl expose pod nginx --type=NodePort --port=80
 
# 포트 확인
kubectl get svc nginx
 
# NSG에서 NodePort 열기
az network nsg rule create \
    --resource-group rg-lab-k8s \
    --nsg-name vm-k8s-worker1NSG \
    --name allow-nodeport \
    --priority 1001 \
    --protocol Tcp \
    --destination-port-ranges 31519

브라우저에서 접속 (http://20.249.10.33:31519)

#Kubernetes Dashboard 배포 및 외부 접속
# Dashboard 설치
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
 
# Admin 계정 + 토큰 생성
kubectl create serviceaccount dashboard-admin -n kubernetes-dashboard
kubectl create clusterrolebinding dashboard-admin \
    --clusterrole=cluster-admin \
    --serviceaccount=kubernetes-dashboard:dashboard-admin
kubectl create token dashboard-admin -n kubernetes-dashboard --duration=24h
 
# NodePort로 변경
kubectl edit svc kubernetes-dashboard -n kubernetes-dashboard
# type: ClusterIP → type: NodePort 로 변경
 
# 포트 확인
kubectl get svc -n kubernetes-dashboard
 
# NSG에서 Dashboard 포트 열기
az network nsg rule create \
    --resource-group rg-lab-k8s \
    --nsg-name vm-k8s-worker1NSG \
    --name allow-dashboard \
    --priority 1002 \
    --protocol Tcp \
    --destination-port-ranges 30695

브라우저에서 접속 (https://20.249.10.33:30695) → 토큰 입력 → Kubernetes Dashboard GUI 접속