
이 포스팅에서 사용된 아키텍처는 마이스터넷 지방기능경기대회 클라우드 직종 2025 제 2과제를 참조하며, 모든 저작권은 마이스터넷(한국산업인력공단)에 있음을 미리 알립니다.
과제 문제지는 마이스터넷 홈페이지에서 확인하실 수 있습니다.
선수 유의사항
다음 유의사항을 고려하여 요구사항을 완성하시오. (불필요한 항목은 중략됨)
- 문제에 제시된 괄호(
<>)는 변수를 뜻함으로 선수가 적절히 변경하여 사용해야 합니다. (환경 변수 등)- Security Group의 80/443 Outbound는 Any Open(
0.0.0.0/0)하여 사용할 수 있도록 합니다. (기본적으로 모든 TCP/UDP 트래픽의 Outbound는0.0.0.0/0으로 허용됨)- Bastion EC2는 채점을 위해 사용합니다 서버들에 접속 문제가 생겨 채점에 불이익을 받지 않도록 합니다. (마지막 과제에서 Bastion Host가 삭제되지 않도록 주의할 것. 문제에 따로 안내되어 있지 않아 SSH 및 키 페어 방식으로 사용하였음)
- 과제 종료 전 실행 중인 테스트 및 부하를 중지하여 서버에 문제가 없도록 해야 합니다. (KEDA 오토스케일링 등)
- Tag 정보가 맞지 않거나 권한 문제가 생겨 채점이 불가하지 않도록 주의합니다. (문제를 유의하여 꼼꼼하게 작업할 수 있도록 해야함)
2025 클라우드 직종 지방기능경기대회 제 2과제에서 해결해야할 세부적인 각 문제는 아래와 같다. (각 문제는 서로 의존하지 않는 별개의 문제임)
첫번째 문제는 AWS Client VPN을 구축하는 것이 주된 문제로, VPC와 Client VPN, EC2 및 SSM(System Manager) Session Manager를 구축해야 한다.
두번째 문제는 EKS 클러스터를 구축하고 KEDA를 통해 Event Driven 오토스케일링을 구축하는 문제로, EKS 클러스터와 SQS 큐, 그리고 채점을 위한 EC2 Bastion Host를 구축해야 한다.
세번째 문제는 Terraform IaC(HCL)을 작성하는 문제로, Terraform을 통해 VPC 및 EC2 등을 구축해야 한다. 마찬가지로 채점을 위한 EC2 Bastion Host 또한 구축해야 한다.
마지막 네번째 문제는 AWS EC2 삭제 자동화 셸 스크립트를 작성하는 문제로, 문제에 유의하여 셸 스크립트를 작성하고 채점을 위한 EC2 Bastion Host를 구축해야한다.
총 경기 시간은 4시간으로, 안전하게 30분을 남기고 과제를 끝낸다는 가정 하에 각각 1시간/1시간/1시간/30분을 투자하면 아주 넉넉하게 풀이할 수 있을 것이다.
몇몇 문제에서 공통적으로 사용되는 IAM 역할은 다음과 같다. EC2AdminRole
문제에서 AdministratorAccess 권한을 가지는 EC2 또는 EC2 Bastion Host를 프로비저닝 해야할 경우 해당 역할을 사용하면 된다.



이 외에 AmazonSSMManagedInstanceCore 권한을 필요로 하는 2개의 문제가 있으나, 하나는 Terraform HCL을 통한 IaC 문제이기 때문에 여기선 생략하겠다.
AWS Client VPN을 통해 Local PC에서 AWS VPC에 접근할 수 있도록 합니다. VPC의 Name(tag)는 ws-vpn-vpc이며, CIDR은
10.99.0.0/16을 사용합니다.VPC 구성
Subnet Name CIDR Internet Access AZ ws-public-subnet-a 10.99.0.0/24IGW ap-northeast-2a ws-public-subnet-b 10.99.1.0/24IGW ap-northeast-2b ws-private-subnet-a 10.99.10.0/24NAT GW ap-northeast-2a ws-private-subnet-b 10.99.11.0/24NAT GW ap-northeast-2b EC2 구성
웹 서비스를 배포하기 위한 EC2를 생성합니다. Private Subnet에 AZ 별로 하나씩 총 2대의 인스턴스를 생성합니다. 채점 시 SSM Session Manager 기능을 통해 인스턴스에 연결할 수 있어야 합니다.
아래 정보를 활용해 생성 후
Welcome to the ws portal이라는 메시지가 담긴index.html파일을 생성하고 웹서버가 보여줄 수 있도록 구성합니다. 최종적으로 인스턴스에서curl http://localhost/index.html호출 시 HTML 파일 내용을 출력해야 합니다.
- EC2 name (Tag): ws-web-a, ws-web-b
- AMI: Amazon Linux 2023
- Instance type: t3.micro
- IAM role: AmazonSSMManagedInstanceCore 권한을 가지는 IAM role 생성
AWS Client VPN
Local PC에서 VPC 내부의 EC2 인스턴스와 통신하기 위해 AWS Client VPN을 구성합니다.
구성이 모두 완료된 후, 웹 브라우저로 ws-web-a 및 ws-web-b 인스턴스의 IP에 접속하였을 때
index.html의 내용이 정상적으로 출력되어야 합니다. VPN 프로필은 미리 구성해둡니다.
- Client VPN name (tag): ws-client-vpn
- Client CIDR:
10.254.0.0/16- Authentication option: mutual authentication 활성화
- Target network subnet: ws-private-subnet-a, ws-private-subnet-b
- Authorization rules: VPC CIDR만 허용
이 문제는 로컬 PC에서 두 프라이빗 EC2에 접근할 수 있도록 Client VPN을 구성하는 문제이다. VPC는 문제에서 제시된 값으로 구성하고, EC2 두 대는 각 프라이빗 서브넷에 배치하며 SSM Role을 통해 웹 서버를 구성한다. (이 풀이에서는 Nginx 사용한다.)
Client VPN 또한 문제에서 제시된 값으로 구성하고, 클라이언트 CIDR은 10.254.0.0/16, Mutual Authentication 활성화 및 AuthZ는 VPC CIDR만 허용한다. 인증서는 EasyRSA를 통해 만들어보겠다.
VPC(ws-vpn-vpc) CIDR:
10.99.0.0/16
Subnet Name CIDR Internet Access AZ ws-public-subnet-a 10.99.0.0/24IGW ap-northeast-2a ws-public-subnet-b 10.99.1.0/24IGW ap-northeast-2b ws-private-subnet-a 10.99.10.0/24NAT GW ap-northeast-2a ws-private-subnet-b 10.99.11.0/24NAT GW ap-northeast-2b (여기서 NAT GW는 고가용성을 위해 AZ 별로 배치하는 것이 좋지만, 풀이에선 비용상 단일 AZ에 배치하도록 하겠다.)
이 과정은 웹 콘솔에서 제공하는 VPC 생성 도구를 활용하겠다. 리소스를 하나하나 전부 만들고 직접 라우트 테이블을 만드는 것 또한 공부의 일종이기 때문에 직접 만들어보는 것을 추천한다.

보안 그룹은 Client VPN 엔드포인트 전용 보안 그룹과 EC2 인스턴스 전용 보안 그룹을 만들어주겠다. 필수는 아니지만, 전자는 Client VPN 엔드포인트의 ENI에 붙고 후자(EC2 전용)는 그 SG를 소스로 허용한다.


- EC2 name (Tag): ws-web-a, ws-web-b
- AMI: Amazon Linux 2023
- Instance type: t3.micro
- IAM role: AmazonSSMManagedInstanceCore 권한을 가지는 IAM role 생성
먼저 AmazonSSMManagedInstanceCore 권한을 가지는 역할을 만들자. 따로 정해진 이름은 없으므로 EC2SSMRole으로 설정하였다.



다음으로 2개의 프라이빗 서브넷에 각각 EC2(ws-web-a/b)를 프로비저닝하자. 대상 그룹 및 로드밸런서를 구성하라는 말이 없었으므로 로드밸런서는 구성하지 않겠다.
두 EC2는 아래와 같이 구성한다. 사진 상 ap-northeast-2a 서브넷에만 배치하지만, 2b에도 프로비저닝해야 한다.

SSM Session Manager를 사용하여 프라이빗 EC2에 접근할 것이기 때문에 키페어는 생략한다.



이 과정을 ws-web-a/b 각각 진행하고, 아래와 같이 잘 생성되었는지 확인하자. 두 EC2 모두 10.99.10.0/24 또는 10.99.11.0/24 CIDR 범위여야 한다.

여기까지 잘 되었다면 아래와 같이 SSM Session Manager에서 두 개의 인스턴스가 확인되어야 한다.

웹 콘솔에서 세션에 접속하거나 아래의 명령어를 통해 접속하여 Nginx를 구성하자.
aws ssm start-session --target <Instance ID>
sudo dnf -y install nginx
echo 'Welcome to the ws portal A/B' | sudo tee /usr/share/nginx/html/index.html
sudo systemctl enable --now nginx
# curl -s http://localhost/index.html

다음으로 Client VPN을 위한 인증서(CA)가 필요한데, 아래의 EasyRSA 명령어를 통해서 만들어보겠다. MacOS 기준으로 /opt/homebrew/etc/easy-rsa/pki 위치에 저장된다. 윈도우의 경우 C:\Program Files\OpenVPN\easy-rsa\pki 등에 저장될 것이다.
easyrsa init-pki
easyrsa build-ca nopass # CN: ws-client-vpn-ca
easyrsa build-server-full server.domain.com nopass
easyrsa build-client-full client1.domain.com nopass
참고로 도메인 이름은 FQDN 형식을 사용해야한다. 왜 FQDN 형식이 아닐 시 ACM에서 가져오기가 정상적으로 되는 것인지는 잘 모르겠으나, 아무튼 FQDN 형태여야하며 실제 존재하지 않는 FQDN을 사용해도 무방하다.
참고: AWS re:Post - Client VPN Endpoint Creation - Not Detecting Client Certificate in ACM
명령어 실행 후 찾기 쉽게 아래와 같이 복사해두자.
./rsa
├── ca.crt
├── issued
│ ├── client1.domain.com.crt
│ └── server.domain.com.crt
└── private
├── ca.key
├── client1.domain.com.key
└── server.domain.com.key
이제 이를 아래와 같이 ACM에 등록해두자. 인증서 본문엔 각 .crt를, 프라이빗 키엔 .key, 그리고 인증서 체인엔 ca.crt 내용을 넣자.

- Client VPN name (tag): ws-client-vpn
- Client CIDR: 10.254.0.0/16
- Authentication option: mutual authentication 활성화
- Target network subnet: ws-private-subnet-a, ws-private-subnet-b
- Authorization rules: VPC CIDR만 허용
Client VPN 엔드포인트는 아래와 같이 구성한다.

보안 그룹은 만들어둔 ws-client-vpn-endpoint-sg를 선택한다.

클라이언트 CIDR은 10.254.0.0/16이다.

그리고 아래와 같이 상호 인증(Mutual Authentication)을 활성화하고, 각각의 인증서 ARN을 선택하면 된다.

이 외에 CloudWatch 로깅 등의 옵션도 있지만, 문제에서 제시된 사항은 아니기 때문에 나머지는 기본값으로 두고 VPN 엔드포인트를 생성하자.
분활 터널(Split Tunnel) 옵션을 체크할 수 있다. 이는 VPN으로 접속하더라도 특정 목적지(AWS)로만 트래픽을 보내고, 나머지는 기존 네트워크를 그대로 사용하는 방식이다.
Full Tunnel과는 반대되는 방식인데, Split Tunnel을 활성화 해두면 VPN 라우트에 정의된 CIDR로만 트래픽을 보내고 그 외의 트래픽은 VPN을 거치지 않으므로 요금적으로 이득이다. 때문에 Full Tunnel VPN이 목적이 아니라면 활성화해두는 것을 추천한다.

그럼 Pending associate라는 메시지가 나올텐데, VPN 엔드포인트를 VPC 서브넷에 연결(Associate)해야한다.
그 전에 문제에서 제시된것 처럼 Authorization Rules에서 VPC CIDR(10.99.0.0/16)만 허용하도록 구성해야하자.


다음으로 서브넷(ws-private-subnet-a)과 연결하도록 하자.


마찬가지로 ws-private-subnet-b도 연결한다. 연결이 Active 되기 까지 5분 이상 소요되는데, 그 동안 기다리면서 아래의 링크에서 Client VPN for Desktop을 다운받는다.

그리고 VPN 엔드포인트 콘솔에서 <클라이언트 구성 다운로드> 버튼을 클릭한다. 그럼 ovpn 확장자의 파일이 다운로드 될텐데, 이를 VSCode나 텍스트 에디터로 열어서 인증서 정보를 수정하자. 아래의 정보를 추가하면 된다.
<cert>
(Client1 인증서 파일 내용)
</cert>
<key>
(Client1 프라이빗 키 파일 내용)
</key>
또한 마지막 줄에 verify-x509-name server.domain.com name 줄은 없애야 한다. 이건 서버 인증서의 CN 또는 SAN에 정확히 server.domain.com이 있어야만 통과되는건데, 괜히 복잡해질 수 있다.
이제 아래와 같이 두 서브넷 모두 Associated 되었다면, AWS VPN Client for Desktop의 File > Manage Profiles > Add Profile 메뉴에서 수정된 ovpn 파일을 선택하고 연결해보자.


프로필을 만들었다면 연결을 시도해보자. 만약 TLS 핸드셰이크 관련 에러가 발생한다면 대부분 인증서 문제이니, 꼼꼼하게 확인하도록 하자.

이제 두 프라이빗 EC2에 접근이 가능한지 확인해보도록 하자. 각 EC2의 프라이빗 IP는 아래와 같다.
10.99.10.109 (Port 80)10.99.11.137 (Port 80)

잘 동작하는 것을 확인해볼 수 있다. 문제를 풀면서 발생하는 대부분의 트러블은 인증서 관련(특히 Server/Client EKU) 문제일테니 트러블슈팅에 참고하도록 하자.
KEDA를 이용해 Pod Autoscaling이 가능하도록 합니다. 주문/배송 서비스(
order-v1.py)에서 주문량에 따라 Pod가 늘어나고 줄어드는 스케일링을 구현해야 합니다.EKS 구성
웹 애플리케이션을 배포하기 위한 EKS 클러스터를 생성합니다. EKS 클러스터를 위해 Default VPC를 사용하거나 별도의 VPC를 새롭게 구성할 수도 있습니다.
EKS 클러스터 상에는 주문을 처리하기 위한 애플리케이션이 배포되어야 합니다. 주어진 바이너리는 Amazon Linux 2023에서 동작을 확인하였습니다. 애플리케이션을 실행하는 방법은 아래의 내용을 참고할 수 있습니다.
- Cluster name: order-cluster
- Cluster version: 1.31 (이 포스팅에선 1.33으로 대신한다.)
- K8s Namespace name: order
- K8s Deployment name: order-processor
- K8s Deployment min Pods 갯수: 1
Environment Variables:
- QUEUE_URL:
<<아래에서 생성한 SQS URL>>- REGION_NAME:
<<애플리케이션을 배포할 region>>EC2 Bastion Host 구성
채점 시 사용하기 위해 Bastion 인스턴스를 생성해야 합니다. 인스턴스는 어느 VPC에 생성해도 무방하지만, EKS 클러스터에 접근 가능하며 AdministratorAccess 권한을 가지고 있어야 합니다.
만약 Bastion 인스턴스에 접근할 수 없거나 EKS 클러스터에 접근할 수 없으면 채점 시 불이익을 받을 수 있습니다.
- EC2 name (tag): order-bastion
- AMI: Amazon Linux 2023
- Instance size: t3.micro
SQS 구성
주문을 수신하고 처리하기 위해 SQS queue를 생성합니다. 문제에서 명시되어 있지 않은 값은 모두 기본값을 사용하도록 합니다.
- SQS Name: order-queue
오토스케일링 구성
SQS에 적재된 메시지의 수에 따라 Pod의 개수가 조정되어야 합니다. KEDA operator를 EKS 클러스터에 설치하여 사용하도록 합니다. 설치 방법은 https://keda.sh/docs/2.15/deploy를 참고합니다.
Operator Pod의 이름은 keda-operator가 포함되어야 합니다. 과제 종료 전 모든 부하를 제거하고 운영 중인 Order Pod는 1대만 있어야 합니다. 초과할 시 채점이 불가능합니다. 부하 주입 후 Pod 증설까지 5분 대기하며, 증설 후 Pod 감소까지도 5분 대기합니다.
- Keda 스케일링 메시지 수(권장): 5
EKS 클러스터를 구축하고 EC2 Bastion Host 및 SQS 큐를 구성, Kubernetes(EKS) 클러스터 내 KEDA를 설치하여 EDA 기반 오토스케일링을 주제로 하는 문제이다. EKS 클러스터와 AWS 리소스인 SQS 사이의 권한은 IRSA로 구성하며, 클러스터는 eksctl 도구를 사용하겠다.
VPC는 eksctl이 만드는 VPC를 그대로 사용하는데, 이 VPC에 EC2 Bastion Host를 구성한다. 때문에 클러스터를 삭제할 땐(Cloudformation 스택 삭제) EC2 Bastion Host를 먼저 종료시켜야 한다.
- Cluster name: order-cluster
- Cluster version: 1.31 (이 포스팅에선 1.33으로 대신한다.)
# cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: order-cluster
region: ap-northeast-2
version: "1.33" # https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html 를 참조하여 Standard Support 버전을 사용하는 것을 권장한다.
vpc:
cidr: 10.0.0.0/16
nat:
gateway: Single
iam:
withOIDC: true
managedNodeGroups:
- name: ng-1
instanceType: t3.medium
desiredCapacity: 2
privateNetworking: true
iam:
withAddonPolicies:
ebs: true
eksctl create cluster -f cluster.yaml
EKS 클러스터를 프로비저닝하는 시간이 오래 걸릴 수 있다. 대충 15분 정도 소요되는데, 그 사이 아래의 작업을 먼저 하고 오자. 구성이 완료되었다면 eksctl이 자동으로 kubectl 설정을 구성해준다. kubectl get nodes 명령어를 통해 EC2 노드를 확인해보자.

EC2 Bastion Host 구성
채점 시 사용하기 위해 Bastion 인스턴스를 생성해야 합니다. 인스턴스는 어느 VPC에 생성해도 무방하지만, EKS 클러스터에 접근 가능하며 AdministratorAccess 권한을 가지고 있어야 합니다.
만약 Bastion 인스턴스에 접근할 수 없거나 EKS 클러스터에 접근할 수 없으면 채점 시 불이익을 받을 수 있습니다.
- EC2 name (tag): order-bastion
- AMI: Amazon Linux 2023
- Instance size: t3.micro
채점 시 해당 Bastion Host로 접속하여 주문 서비스 API를 호출한다. 때문에 같은 VPC 내에 있어야하고, AdministratorAccess 권한을 가져야 한다. 역할은 EC2AdminRole을 사용하자. EC2 Bastion Host는 아래와 같이 구성한다.


VPC는 eksctl이 자동으로 생성한 VPC를, 서브넷은 그 중 퍼블릭 서브넷을 선택하자. 아직 안보인다면 Cloudformation 스택이 아직 VPC 리소스를 만든 상태가 아닌 것이니 조금만 더 기다리고 새로고침하자.


이 상태로 EC2 Bastion Host를 프로비저닝하면 된다.

마지막에 접속 후 EKS 클러스터 내 Service에 접근이 가능한지 테스트해볼 것이다.
SQS 구성
주문을 수신하고 처리하기 위해 SQS queue를 생성합니다. 문제에서 명시되어 있지 않은 값은 모두 기본값을 사용하도록 합니다.
- SQS Name: order-queue
SQS는 큐만 생성하고 그 외의 구성은 문제에 적혀있듯이 기본 값을 사용한다.

- K8s Namespace name: order
- K8s Deployment name: order-processor
- K8s Deployment min Pods 갯수: 1
Environment Variables:
- QUEUE_URL:
<<아래에서 생성한 SQS URL>>- REGION_NAME:
<<애플리케이션을 배포할 region>>
대회에서 제공되는 Order 애플리케이션의 소스코드나 바이너리, 컨테이너 이미지 등이 없기 때문에 여기선 다른 이미지로 대체한다. (Bastion Host에서 접근할 수 있는지 확인 용도)
또한 KEDA 테스트에선 애플리케이션에서 컨슈밍하는 것이 아닌 AWS CLI를 통해 메시지를 보내고 삭제하여 테스트해볼 것이다.
먼저 네임스페이스 order를 만들자. kubectl create namespace order 명령어를 사용해도 되고 매니페스트를 작성해도 된다. 네임스페이스를 만들었다면 환경 변수 저장을 위한 Secret 또는 ConfigMap을 만드는데, 값을 Base64로 인코딩해야 하므로 echo "value" | base 명령어를 사용하여 설정하거나 kubectl create secret 명령어를 사용하자.
# secret.yaml (kubectl apply -f secret.yaml)
apiVersion: v1
kind: Secret
metadata:
name: order-processor-secrets
namespace: order
type: Opaque
data:
QUEUE_URL: aHR0cHM6Ly9zcXMuYXAtbm9ydGhlYXN0LTIuYW1hem9uYXdzLmNvbS85ODYxMjk1NTg5NjYvb3JkZXItcXVldWUK # echo "SQS_큐_주소" | base64
REGION_NAME: YXAtbm9ydGhlYXN0LTIK # echo "ap-northeast-2" | base64
Deployment는 아래와 같다. KEDA에서 HPA를 만들어 스케일링을 하는 원리이기 때문에 replicas는 0으로 설정한다. 이미지는 대회와는 다른 이미지로 대체하며, GET /env?select=QUEUE_RUL,REGION_NAME으로 환경 변수를 확인할 수 있다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-processor
namespace: order
labels:
app: order-processor
spec:
replicas: 0
selector:
matchLabels:
app: order-processor
template:
metadata:
labels:
app: order-processor
spec:
containers:
- name: demo # 테스트 용도로만 사용, 실제론 대회에서 제공되는 이미지 사용
image: rlawnsdud/demo
ports:
- containerPort: 3000
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "3000"
- name: QUEUE_URL
valueFrom:
secretKeyRef:
name: order-processor-secrets
key: QUEUE_URL
- name: REGION_NAME
valueFrom:
secretKeyRef:
name: order-processor-secrets
key: REGION_NAME
마지막으로 Service는 따로 말이 없었으나, 테스트 용도로 NodePort 타입의 Service를 만들어주었다. 같은 네트워크(VPC) 내의 Bastion Host에서 채점하니 LoadBalancer 등을 만들 필요는 없을 것 같다.
apiVersion: v1
kind: Service
metadata:
name: order-processor-service
namespace: order
spec:
type: NodePort
selector:
app: order-processor
ports:
- protocol: TCP
port: 80
targetPort: 3000
nodePort: 30080

오토스케일링 구성
SQS에 적재된 메시지의 수에 따라 Pod의 개수가 조정되어야 합니다. KEDA Operator를 EKS 클러스터에 설치하여 사용하도록 합니다. 설치 방법은 https://keda.sh/docs/2.15/deploy를 참고합니다.
Operator Pod의 이름은 keda-operator가 포함되어야 합니다. 과제 종료 전 모든 부하를 제거하고 운영 중인 Order Pod는 1대만 있어야 합니다. 초과할 시 채점이 불가능합니다. 부하 주입 후 Pod 증설까지 5분 대기하며, 증설 후 Pod 감소까지도 5분 대기합니다.
- Keda 스케일링 메시지 수(권장): 5
KEDA에 대한 자세한 내용은 https://velog.io/@yulmwu/kubernetes-keda 포스팅을 참고하자. 여기선 Helm을 통한 설치 방법과 IRSA 구성 방법만 서술한다.
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
다음으로 KEDA가 SQS에 접근할 수 있도록 IRSA를 구성한다.
(keda-sqs-scaler-policy.json)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadSqsQueueAttributesForScaling",
"Effect": "Allow",
"Action": ["sqs:GetQueueAttributes", "sqs:GetQueueUrl"],
"Resource": "*"
}
]
}
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
CLUSTER_NAME="order-cluster"
aws iam create-policy \
--policy-name "KedaSqsScalerPolicy" \
--policy-document file://keda-sqs-scaler-policy.json \
--query 'Policy.Arn' --output text
eksctl create iamserviceaccount \
--cluster $CLUSTER_NAME \
--namespace keda \
--name keda-operator \
--role-name "keda-operator-role" \
--attach-policy-arn "arn:aws:iam::${ACCOUNT_ID}:policy/KedaSqsScalerPolicy" \
--role-only \
--approve \
--region ap-northeast-2
helm upgrade --install keda kedacore/keda \
--namespace keda \
--create-namespace \
--set podIdentity.aws.irsa.enabled=true \
--set podIdentity.aws.irsa.roleArn="arn:aws:iam::${ACCOUNT_ID}:role/keda-operator-role"
kubectl rollout restart deploy keda-operator -n keda
다음으로 KEDA가 SQS에 접근하기 위한 인증 정보와 ScaledObject를 구성해야 하는데, ScaledObject의 경우 기존의 Deployment를 참조해야 한다. (order-processor)
# keda.yaml
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: aws-irsa-keda-operator
namespace: order
spec:
podIdentity:
provider: aws
identityOwner: keda
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: sqs-scaledobject
namespace: order
spec:
scaleTargetRef:
name: order-processor
pollingInterval: 10
cooldownPeriod: 10
minReplicaCount: 1
maxReplicaCount: 5
triggers:
- type: aws-sqs-queue
authenticationRef:
name: aws-irsa-keda-operator
metadata:
queueURL: "https://sqs.ap-northeast-2.amazonaws.com/<ACCOUNT_ID>/order-queue"
queueLength: "5" # Keda 스케일링 메시지 수(권장): 5
activationQueueLength: "0"
awsRegion: "ap-northeast-2"
최대 몇개의 Pod가 복제되어야 하는지가 안내되어 있지 않기 때문에 최대 5개의 Pod로 구성하였다. 단, 최소 1개의 Pod는 운영되어야 하므로 minReplicaCount는 1로 설정해두었다.
문제가 없는지 KEDA Operator를 확인해보도록 하자.
kubectl -n keda get pod | grep 'keda-operator'
kubectl -n keda logs pod/keda-operator-12345 | tail -n 10

아래와 같이 order 네임스페이스의 Pod 목록을 조회하였을 때 하나만 나와야 한다. 채점 시 Order Processor Pod는 하나만 있어야 하니 주의하도록 하자.
다음으로 아래의 명령어를 통해 SQS 큐에 메시지를 보내보자. 20개를 보냈으니 총 4~5개의 Pod가 운영되어야 한다.
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
SQS_QUEUE_URL="https://sqs.ap-northeast-2.amazonaws.com/$ACCOUNT_ID/order-queue"
for i in $(seq 1 20); do
aws sqs send-message --queue-url "$SQS_QUEUE_URL" --message-body "msg-$i" --region "ap-northeast-2" >/dev/null
done

이제 메세지들을 삭제(Purge)하여 Order Processor Pod가 1개로 감소하는지 확인해보자.
aws sqs purge-queue --queue-url "$SQS_QUEUE_URL" --region "ap-northeast-2"

이렇게 KEDA가 동작하는걸 확인해봤다면, Bastion Host에서 EKS 클러스터에 접근할 수 있는지도 확인해보자. NodePort를 확인한다. (두 노드 중 아무 노드의 IP로 접근해도 된다.)

이 노드의 프라이빗 IP를 통해 Bastion Host에서 호출이 되는지 확인해보자. 잘 출력된다면 성공이다.

Terraform을 활용해 VPC, EC2 등 클라우드 인프라를 생성하고자 합니다. Terraform 코드를 작성합니다.
Bastion 구성
채점 시 사용하기 위해 Bastion 인스턴스를 생성합니다. Bastion 인스턴스는 Default VPC에 배포하며, AdministratorAccess 권한을 가지고 있어야 합니다.
모든 Terraform Code는
/home/ec2-user/korea/경로에 위치해야 합니다. 채점 전 해당 경로에서terraform init,terraform destroy,terraform apply명령어를 실행하여 생성된 리소스 삭제 후 채점합니다.
- EC2 Name: korea-bastion
- AMI: Amazon Linux 2023
- EC2 type: t3.micro
Terraform 구성
Terraform code를 작성하여 아래의 주어진 VPC, EC2, IAM 리소스들을 생성하여야 합니다.
만약 사전에 생성된 리소스가 있는 경우 리소스 이름이 중복되어 Terraform 명령어에 오류가 발생할 수 있으므로 채점 전에 Default VPC와 Bastion을 제외한 모든 리소스를 삭제하도록 합니다.
Terraform으로 생성된 모든 리소스는
Proejct=KoreaSkills태그를 갖도록 구성합니다.
- VPC name (tag): korea-vpc
- VPC CIDR:
10.0.0.0/16- Public subnet name (tag): korea-public-subnet-a (ap-northeast-2a zone 이용)
- Public subnet CIDR:
10.0.0.0/24EC2 구성은 아래와 같습니다.
- EC2 name (tag): korea-instance
- EC2 AMI: Amazon Linux 2023
- EC2 type: t3.micro
- EC2 subnet: korea-public-subnet-a
- EC2 EBS encryption: 활성화
- EC2 Public IP: 자동 할당
- EC2 IAM role: AmazonSSMManagedInstanceCore 권한 가지도록 구성 (SSM session manager 연결 가능)
여기서 명시되지 않은 내용이 있는데, EC2는 Public IP가 할당되도록 한다. 이것이 아니라면 SSM Session Manager를 위한 VPC 엔드포인트나 NAT Gateway를 추가적으로 만들어야 하는데, 이에 대한 내용이 없으므로 Public IP를 할당하는 방식을 선택하겠다.
Bastion 구성
채점 시 사용하기 위해 Bastion 인스턴스를 생성합니다. Bastion 인스턴스는 Default VPC에 배포하며, AdministratorAccess 권한을 가지고 있어야 합니다.
모든 Terraform Code는
/home/ec2-user/korea/경로에 위치해야 합니다. 채점 전 해당 경로에서terraform init,terraform destory,terraform apply명령어를 실행하여 생성된 리소스 삭제 후 채점합니다.
- EC2 Name: korea-bastion
- AMI: Amazon Linux 2023
- EC2 type: t3.micro
Bastion Host를 프로비저닝하기 전, AdministratorAccess 권한을 가지고 있어야 한다고 한다. EC2AdminRole 역할을 사용하면 된다.
키페어 파일은 임의로 만든 키페어로 진행하고, 이는 실제 대회와는 다를 수 있다. 제시된 상황에 맞게 해야한다.




위와 같은 EC2 Bastion Host를 프로비저닝하였다면 /home/ec2-user/korea/ 디렉토리를 만들고 Terraform을 설치해야 한다.
chmod 400 common.pem
ssh -i common.pem ec2-user@<Bastion Public IP> # Amazon Linux의 기본 사용자 이름은 ec2-user이다.
# Bastion Host
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo dnf -y install terraform
mkdir /home/ec2-user/korea
cd /home/ec2-user/korea

이후 Terraform 코드를 SSH로 접속된 Bastion Host 내에서 Vim, Nano 등을 사용하여 직접 작성하거나, 로컬 PC에서 작성하고 SCP 등의 명령어로 전송할 수 있다.
대회 환경에선 VSCode를 제공하고 VSCode 내 하이라이팅 및 자동완성 익스텐션을 활용하면 좋기 때문에 로컬 PC에서 작성 후 SCP 명령어를 통해 EC2 Bastion Host로 전송하는 방식을 택하겠다.
Terraform 구성
Terraform code를 작성하여 아래의 주어진 VPC, EC2, IAM 리소스들을 생성하여야 합니다.
만약 사전에 생성된 리소스가 있는 경우 리소스 이름이 중복되어 Terraform 명령어에 오류가 발생할 수 있으므로 채점 전에 Default VPC와 Bastion을 제외한 모든 리소스를 삭제하도록 합니다.
Terraform으로 생성된 모든 리소스는
Proejct=KoreaSkills태그를 갖도록 구성합니다.
- VPC name (tag): korea-vpc
- VPC CIDR:
10.0.0.0/16- Public subnet name (tag): korea-public-subnet-a (ap-northeast-2a zone 이용)
- Public subnet CIDR:
10.0.0.0/24EC2 구성은 아래와 같습니다.
- EC2 name (tag): korea-instance
- EC2 AMI: Amazon Linux 2023
- EC2 type: t3.micro
- EC2 subnet: korea-public-subnet-a
- EC2 EBS encryption: 활성화
- EC2 IAM role: AmazonSSMManagedInstanceCore 권한 가지도록 구성 (SSM session manager 연결 가능)
Terraform HCL 코드의 디렉토리 구조는 아래와 같다. (Backend는 S3 버킷을 사용하겠다.)
terraform
├─ provider.tf
├─ backend.tf
├─ vpc.tf
└─ ec2.tf
# terraform/provider.tf
provider "aws" {
region = "ap-northeast-2"
}
# terraform/backend.tf
terraform {
backend "s3" {
bucket = "yulmwu-terraform-backend"
key = "korea/terraform.tfstate"
region = "ap-northeast-2"
}
}
위 코드 중 Bucket은 본인의 S3 Bucket 이름 변경해야한다.
- VPC name (tag): korea-vpc
- VPC CIDR:
10.0.0.0/16- Public subnet name (tag): korea-public-subnet-a (ap-northeast-2a zone 이용)
- Public subnet CIDR:
10.0.0.0/24
문제 설명 중 korea-public-subnet-a 이외의 서브넷은 없었고, 때문에 Terraform에서도 해당 서브넷 하나만 만들겠다. 모든 리소스엔 Project=KoreaSkills 태그가 붙도록 구성하라는 말이 있었기 때문에 각 리소스마다 태그를 지정해주었다.
# terraform/vpc.tf
resource "aws_vpc" "korea_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "korea-vpc"
Project = "KoreaSkills"
}
}
resource "aws_subnet" "korea_public_subnet_a" {
vpc_id = aws_vpc.korea_vpc.id
cidr_block = "10.0.0.0/24"
availability_zone = "ap-northeast-2a"
map_public_ip_on_launch = true
tags = {
Name = "korea-public-subnet-a"
Project = "KoreaSkills"
}
}
VPC 리소스(aws_vpc) 중 map_public_ip_on_launch 필드는 해당 서브넷 내에서 생성되는 EC2엔 자동으로 Public IP가 붙도록 하는 옵션이다. 퍼블릭 서브넷이기 때문에 해당 옵션을 활성화하였다. (기본값은 false)
다음으로 IGW 및 라우트 테이블 리소스를 생성해주고 연결하자. 그럼 기초적인 VPC 리소스가 생성된다.
resource "aws_internet_gateway" "korea_igw" {
vpc_id = aws_vpc.korea_vpc.id
tags = {
Name = "korea-igw"
Project = "KoreaSkills"
}
}
resource "aws_route_table" "korea_public_rt" {
vpc_id = aws_vpc.korea_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.korea_igw.id
}
tags = {
Name = "korea-public-rt"
Project = "KoreaSkills"
}
}
resource "aws_route_table_association" "korea_public_rt_assoc" {
subnet_id = aws_subnet.korea_public_subnet_a.id
route_table_id = aws_route_table.korea_public_rt.id
}
- EC2 name (tag): korea-instance
- EC2 AMI: Amazon Linux 2023
- EC2 type: t3.micro
- EC2 subnet: korea-public-subnet-a
- EC2 EBS encryption: 활성화
- EC2 IAM role: AmazonSSMManagedInstanceCore 권한 가지도록 구성 (SSM session manager 연결 가능)
먼저 EC2의 AMI는 Amazon Linux 2023으로 고정하라고 명시되어 있기 때문에 아래와 같이 AMI를 찾는다.
# terraform/ec2.tf
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
다음으로 SSM Session Manager 연결을 위한 AmazonSSMManagedInstanceCore 권한을 가진 역할과 IAM 인스턴스 프로필을 만들자.
resource "aws_iam_role" "korea_ec2_role" {
name = "korea-ec2-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
tags = {
Project = "KoreaSkills"
}
}
resource "aws_iam_role_policy_attachment" "korea_ssm_attach" {
role = aws_iam_role.korea_ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "korea_instance_profile" {
name = "korea-ec2-instance-profile"
role = aws_iam_role.korea_ec2_role.name
}
다음으로 아래와 같은 코드를 통해 조건에 맞는 EC2를 프로비저닝할 수 있다.
resource "aws_instance" "korea_instance" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.korea_public_subnet_a.id
iam_instance_profile = aws_iam_instance_profile.korea_instance_profile.name
root_block_device {
encrypted = true
}
tags = {
Name = "korea-instance"
Project = "KoreaSkills"
}
}
root_block_device는 루트 볼륨을 의미하는데, 즉 기본 EBS 볼륨을 의미한다. EBS 암호화를 활성화하라고 하였으니 encrypted를 활성화해두었다.
이렇게 Terraform 코드를 모두 작성하였다면 SCP 명령어를 통해 Bastion Host 내로 전송하자.
# 경로는 본인의 경로에 맞게 수정하자.
scp -i ./common2.pem terraform/* ec2-user@13.125.165.109:/home/ec2-user/korea


이제 Terraform 코드가 잘 동작하는지 확인해보자. /home/ec2-user/korea 경로에서 아래의 명령어를 통해 Terraform Plan을 확인한다. 만약 AWS STS 관련 에러가 발생한다면 IAM 역할 연결이 제대로 안되었을 가능성이 크니 확인해보자.
terraform init
terraform plan

Plan 시 에러가 발생하지 않는다면 terraform apply 명령어를 통해 적용해보자. 적용 후 확인 요소는 아래와 같다.

S3 백엔드로 사용된 버킷 확인 시 korea/terraform.tfstate 파일이 존재해야 한다.

VPC 리소스는 아래와 같이 리스소 맵을 통해 쉽게 확인할 수 있다. CIDR 등에 문제가 없는지 확인해보자.

EC2 또한 아래와 같이 정상적으로 프로비저닝되었는지 확인해보자. 보안 그룹 중 인바운드 규칙에 아무것도 없어야 정상이다. SSM Session Manager는 특정 프로토콜을 허용해줄 필요가 없다.

SSM Session Manager에선 korea-instance EC2가 보이는지 확인해보자. korea-bastion 또한 관리자 권한을 주었기 때문에 보이는건 정상이다.

만약 보이지 않는다면 IAM 역할이 제대로 붙지 않았거나 Public IP가 없을 경우가 대부분이니 확인해보자. SSM Session Manager 연결 확인도 해보자. 웹 콘솔 또는 아래의 CLI 명령어로 연결이 가능하다.
aws ssm start-session --target <Instance ID>

확인이 되었다면 마지막으로 Terraform Destroy를 해줘야한다. 채점 시 Terraform으로 생성된 리소스가 존재하면 안된다.

제대로 지웠졌나 확인해보려면 VPC를 확인해보면 된다. korea-vpc가 지웠다면 정상이다.

EC2 삭제 자동화를 구성합니다. 해당 문제는 ap-northeast-1(도쿄) 지역에 리소스를 생성합니다.
Default VPC가 지정되어 있어
aws ec2 run-instances명령어로 EC2 생성 시 별도 VPC ID를 지정하지 않더라도 EC2가 생성되도록 합니다.EC2 Bastion Host 구성
채점 시 사용할 EC2 Bastion Host를 생성합니다. Default VPC에 배포되어야 하며, AdministratorAccess 권한을 가지고 있어야 합니다.
또한 자동화 스크립트 실행 시 실수로 삭제되지 않도록 조처를 해야 합니다. 만약 Bastion 인스턴스가 삭제된 경우 이후 채점이 불가하므로 주의하도록 합니다.
- EC2 name (tag): automation-bastion
- EC2 termination protetion: 활성화
- EC2 AMI: Amazon Linux 2023
- EC2 Type: t3.micro
자동화 구성
불필요한 EC2 instance를 삭제할 수 있는 2개의 자동화 스크립트를 작성해야 합니다. 작성한 스크립트는
/home/ec2-user/ec2-automation/경로에 위치해야 하며, 이 외의 경로에 존재하는 스크립트는 채점하지 않습니다.
delete_old_instance.sh스크립트
Project=skills2022라는 태그가 부여된 모든 인스턴스를 삭제합니다.- 만약 조건에 맞지 않는 다른 인스턴스를 삭제한 경우 감점될 수 있습니다.
delete_all_instance.sh스크립트
- EC2 Bastion Host를 제외한 계정에 존재하는 모든 인스턴스를 삭제합니다.
- 스크립트 실행 후 EC2 Bastion Host 외에 실행 및 중지된 인스턴스 존재 시 감점될 수 있습니다.
문제에서 ap-northeast-1 리전을 사용한다고 하니 웹 콘솔에서 리전을 변경하고, CLI 명령어에서 ap-northeast-1 리전을 따로 지정해야한다. (기본 리전이 ap-northeast-2일 경우)

그리고 기본 Default VPC가 있어야 한다고 하는데, 따로 건든게 없다면 넘어가도 된다. 만약 Default VPC가 없다면 웹 콘솔이나 CLI 명령어로 만들면 된다.
aws ec2 describe-vpcs --region ap-northeast-1
aws ec2 create-default-vpc --region ap-northeast-1

채점 시 사용할 EC2 Bastion Host를 생성합니다. Default VPC에 배포되어야 하며, AdministratorAccess 권한을 가지고 있어야 합니다.
또한 자동화 스크립트 실행 시 실수로 삭제되지 않도록 조처를 해야 합니다. 만약 Bastion 인스턴스가 삭제된 경우 이후 채점이 불가하므로 주의하도록 합니다.
- EC2 name (tag): automation-bastion
- EC2 termination protetion: 활성화
- EC2 AMI: Amazon Linux 2023
- EC2 Type: t3.micro
Bastion Host를 프로비저닝하기 전, AdministratorAccess 권한을 가지고 있어야 한다고 한다. 이는 EC2AdminRole 역할을 사용하면 된다.
다음으로 EC2 Bastion Host를 프로비저닝하자. 키페어 파일은 임의로 만든 키페어로 진행하고, 실제 대회와는 다를 수 있다. 제시된 상황에 맞게 해야한다.







불필요한 EC2 instance를 삭제할 수 있는 2개의 자동화 스크립트를 작성해야 합니다. 작성한 스크립트는
/home/ec2-user/ec2-automation/경로에 위치해야 하며, 이 외의 경로에 존재하는 스크립트는 채점하지 않습니다.
delete_old_instance.sh스크립트
Project=skills2022라는 태그가 부여된 모든 인스턴스를 삭제합니다.- 만약 조건에 맞지 않는 다른 인스턴스를 삭제한 경우 감점될 수 있습니다.
delete_all_instance.sh스크립트
- EC2 Bastion Host를 제외한 계정에 존재하는 모든 인스턴스를 삭제합니다.
- 스크립트 실행 후 EC2 Bastion Host 외에 실행 및 중지된 인스턴스 존재 시 감점될 수 있습니다.
두 가지 셸 스크립트를 작성하고 이를 만든 EC2 Bastion Host의 /home/ec2-user/ec2-automation 위치에 저장해야한다. 로컬 PC에서 작성하고 SCP 등의 명령어로 스크립트를 보내거나 SSH로 접속하여 직접 Vim, Nano 등을 사용하여 작성할 수 있다. 후자의 방식이 더욱 더 간단하므로 Nano를 사용하겠다.
chmod 400 common.pem
ssh -i common.pem ec2-user@<Bastion Public IP> # Amazon Linux의 기본 사용자 이름은 ec2-user이다.
# Bastion Host
mkdir /home/ec2-user/ec2-automation
cd /home/ec2-user/ec2-automation
nano delete_old_instance.sh # 또는 vi 등
nano delete_all_instance.sh
delete_old_instance.sh먼저 Project=skills2022 라는 태그가 부여된 모든 인스턴스를 삭제하는 스크립트를 작성해보겠다. EC2를 종료시키는 CLI 명령어는 aws ec2 terminate-instances 인데, aws ec2 describe-instances와는 다르게 --filters 등의 옵션을 줄 수 없고 --instance-ids 옵션만 받는다.
즉 aws ec2 describe-instances로 인스턴스 ID 목록을 가져와 aws ec2 terminate-instances로 종료시켜야 한다. 조건에 맞는 인스턴스 ID 목록을 반환하는 명령어는 아래와 같다.
IDs=$(aws ec2 describe-instances \
--region ap-northeast-1 \
--filters "Name=tag:Project,Values=skills2022" \
--query "Reservations[].Instances[].InstanceId" \
--output text)
# --query 옵션을 빼고 실행해본 다음 출력을 확인하고 --query 옵션을 추가해도 무방하다.
이제 이렇게 반환된 목록을 통해 아래와 같이 aws ec2 describe-instances를 실행한다.
aws ec2 terminate-instances \
--region ap-northeast-1 \
--instance-ids $IDs
delete_all_instance.sh다음으로 EC2 Bastion Host를 제외한 계정에 존재하는 모든 인스턴스를 삭제하는 스크립트를 작성해야 하는데, ap-northeast-1 리전에 존재하는 Bastion Host는 Name=tag:Name,Values=automation-bastion 하나밖에 없으므로 이를 제외한 나머지 모든 인스턴스를 삭제하는 스크립트를 작성하면 된다.
IDs=$(aws ec2 describe-instances \
--region ap-northeast-1 \
--query "Reservations[].Instances[?!(Tags[?Key=='Name' && Value=='automation-bastion'])].InstanceId" \
--output text)
AWS CLI 명령어의 --filters 옵션은 항상 포함된 값만 필터링하기 때문에 문제의 조건을 만족시키기 위해선 --filters 옵션 사용이 불가능하다. 때문에 --query 옵션의 JMESPath 기능을 아래와 같이 활용하면 된다.
?!(Tags[?Key=='Name' && Value=='automation-bastion'])
마지막으로 종료 명령어(terminate-instances)는 아까와 동일하다.
aws ec2 terminate-instances \
--region ap-northeast-1 \
--instance-ids $IDs
이제 아래와 같이 두 스크립트가 올바른 위치에 존재하는지, 스크립트는 올바른지 확인하자.

위와 같이 2개의 스크립트를 작성까지 완료하였다면 제 2과제에 대한 풀이는 완료되었다. 총합 4시간이면 넉넉하게 풀이할 수 있고, 살짝의 여유와 꼼꼼하게 재검토하는 시간까지 고려하여 30분 정도는 남겨두고 과제 풀이를 완료하는 것을 목표로 하면 된다.