2024. 7. 13. 21:13ㆍEKS@Terraform
- CloudNet에서 주관하는 Terraform 스터디 내용입니다
- 내용은 위 책 테라폼으로 시작하는 IaC 를 기준으로 정리하였습니다.
- 실습은 M1 macbook air 에서 진행했습니다.
- 매번 좋은 스터디를 진행해 주시는 CloudNet 팀 감사드립니다
- 잘못된 점, 업데이트된 지식, 다른 코멘트 언제나 환영입니다!
Runner란?
Terraform 에 GitOps 를 적용하기 위한 도구이다. (Terraform 자체도 Devops를 위한 툴인데, 데봅스를 위한 데봅스...?)
스터디장 가시다님은 Workflow 랑 비슷한데 기능이 아직 좀 모자란 정도로 설명하셨다.
Terraform 을 이용한 리소스 관리를 여러 사람이 하게 되면 한 레포에 여러사람이 작업할때 고려해야할 점이 많아지듯 형상관리와 backend lock 등이 필요해진다. 이 기능을 지원하는 것이 Runner 이고, Runner의 한 종류인 Atlantis 는 plan 과 apply 를 PR 코멘트에서 하도록 지원한다.
국내기업의 Atlantis 도입 후기는 펫프렌즈의 사례를 참고해도 좋겠다.
Atlantis 배포
atlantis 환경을 구축하고 여기서 GitOps 를 실습해보자
우선 atlantis의 서버 역할을 하는 EC2 인스턴스를 하나 띄워준다.
EC2 는 cloudformation file 로 구성되어 있다. 아래 토글해놓은 t101-atlantis-ec2.yaml 파일을 저장하고, aws cli 로 cloudformation 을 배포할 것이다. 실습에서 사용할 git, terraform 등의 버전을 통일해서 Gasida 님이 cloudformation 파일을 구성해놓으셨다. 감사합니다!
t101-atlantis-ec2.yaml
AWSTemplateFormatVersion: '2010-09-09'
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "<<<<< Deploy EC2 >>>>>"
Parameters:
- KeyName
- SgIngressSshCidr
- MyInstanceType
- LatestAmiId
- Label:
default: "<<<<< Region AZ >>>>>"
Parameters:
- TargetRegion
- AvailabilityZone1
- AvailabilityZone2
- Label:
default: "<<<<< VPC Subnet >>>>>"
Parameters:
- VpcBlock
- PublicSubnet1Block
- PublicSubnet2Block
Parameters:
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH access to the instances.
Type: AWS::EC2::KeyPair::KeyName
ConstraintDescription: must be the name of an existing EC2 KeyPair.
SgIngressSshCidr:
Description: The IP address range that can be used to communicate to the EC2 instances.
Type: String
MinLength: '9'
MaxLength: '18'
Default: 0.0.0.0/0
AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.
MyInstanceType:
Description: Enter EC2 Type(Spec) Ex) t3.micro.
Type: String
Default: t3.medium
LatestAmiId:
Description: (DO NOT CHANGE)
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
Default: '/aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id'
AllowedValues:
- /aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id
TargetRegion:
Type: String
Default: ap-northeast-2
AvailabilityZone1:
Type: String
Default: ap-northeast-2a
AvailabilityZone2:
Type: String
Default: ap-northeast-2c
VpcBlock:
Type: String
Default: 10.10.0.0/16
PublicSubnet1Block:
Type: String
Default: 10.10.1.0/24
PublicSubnet2Block:
Type: String
Default: 10.10.2.0/24
Resources:
# VPC
TerraformVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcBlock
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: Terraform-VPC
# PublicSubnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Ref AvailabilityZone1
CidrBlock: !Ref PublicSubnet1Block
VpcId: !Ref TerraformVPC
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: Terraform-PublicSubnet1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Ref AvailabilityZone2
CidrBlock: !Ref PublicSubnet2Block
VpcId: !Ref TerraformVPC
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: Terraform-PublicSubnet2
InternetGateway:
Type: AWS::EC2::InternetGateway
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref TerraformVPC
PublicSubnetRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref TerraformVPC
Tags:
- Key: Name
Value: Terraform-PublicSubnetRouteTable
PublicSubnetRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicSubnetRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicSubnetRouteTable
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicSubnetRouteTable
# EC2 Hosts
EC2SG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Terraform EC2 Security Group
VpcId: !Ref TerraformVPC
Tags:
- Key: Name
Value: Terraform-SG
SecurityGroupIngress:
- IpProtocol: '-1'
CidrIp: !Ref SgIngressSshCidr
- IpProtocol: tcp
FromPort: 4141
ToPort: 4141
CidrIp: 0.0.0.0/0
EC21:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref MyInstanceType
ImageId: !Ref LatestAmiId
KeyName: !Ref KeyName
Tags:
- Key: Name
Value: Atlantis
NetworkInterfaces:
- DeviceIndex: 0
SubnetId: !Ref PublicSubnet1
GroupSet:
- !Ref EC2SG
AssociatePublicIpAddress: true
PrivateIpAddress: 10.10.1.10
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
VolumeType: gp3
VolumeSize: 30
DeleteOnTermination: true
UserData:
Fn::Base64:
!Sub |
#!/bin/bash
hostnamectl --static set-hostname Atlantis
# Config convenience
echo 'alias vi=vim' >> /etc/profile
echo "sudo su -" >> /home/ubuntu/.bashrc
# Install Packages & Terraform
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
apt update -qq && apt install tree jq unzip zip terraform -y
# Install aws cli version2
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Install atlantis
wget https://github.com/runatlantis/atlantis/releases/download/v0.28.3/atlantis_linux_amd64.zip -P /root
unzip /root/atlantis_linux_amd64.zip -d /root && rm -rf /root/atlantis_linux_amd64.zip
Outputs:
eksctlhost:
Value: !GetAtt EC21.PublicIp
그리고 aws ec2 권한이 있는 key-pair가 필요하다. 없다면 링크 를 참고해서 키를 생성해준다. 나는 만들어둔 키가 있어서 사용했다
이런 식으로 MYKEYNAME 을 환경변수로 등록하고 aws cli의 cloudformation으로 배포하면 배포가 수행된다
(AWS GUI 콘솔을 사용할 수도 있다)
## cloudformation 배포
aws cloudformation deploy \
--template-file t101-atlantis-ec2.yaml \
--stack-name t101 \
--parameter-overrides KeyName=$MYKEYNAME \
SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 \
--region ap-northeast-2
## pub IP 확인
aws cloudformation describe-stacks \
--stack-name t101 \
--query 'Stacks[*].Outputs[0].OutputValue' \
--output text
배포가 바로 되지는 않기 때문에 (약 3분) 배포를 모니터링하는 코드를 다른 터미널에서 띄워서 보고 있을 수도 있다
while true; do
date
AWS_PAGER="" aws cloudformation list-stacks \
--stack-status-filter CREATE_IN_PROGRESS CREATE_COMPLETE CREATE_FAILED DELETE_IN_PROGRESS DELETE_FAILED \
--query "StackSummaries[*].{StackName:StackName, StackStatus:StackStatus}" \
--output table
sleep 1
done
배포 종료 후 공인 IP 로 접속할 것이므로 주소를 확인해둔다
배포된 EC2로 접속하는데, 공인IP를 알지 않아도 cloudformation 쿼리로 접근할 수 있다는 점이 인상적이었다.
ssh -i ~/.ssh/sshpark.pem ubuntu@$(aws cloudformation describe-stacks \
--stack-name t101 \
--query 'Stacks[*].Outputs[0].OutputValue' \
--output text)
root 계정으로 접근에 성공했다!
이후 사용을 위해 aws configure로 자격증명을 설정한다
여담으로 key 확인이 너무 오랜만이라 secret key를 저장해놓은 위치를 잊어서 조금 헤멨다. 키 관리를 잘합시다!
그리고 현재IP로 접근을 쉽게 하기위해서 환경변수에 현재 IP:4141 포트를 등록해 놓는다
URL="http://$(curl -s ipinfo.io/ip):4141"
echo $URL
이제 테라폼 코드를 관리할 Github Repository 를 생성한다
그리고 나서 이 레포에서 사용할 토큰을 새로 만들어준다 (실습용이므로 7일짜리로 만들었다)
새로 생성한 토큰을 터미널에 변수로 등록한다.
등록할 때 SECRET 변수에 아무 문장을 만들어서 추가로 등록한다
그리고 생성한 repo에 웹훅을 추가한다. 웹훅이란 웹훅이란 데이터가 변경되었을 때 실시간으로 알림을 받을 수 있는 기능이다.
URL 에는 터미널 변수로 등록한 URL/events 를 넣어주고, 알람 받는 이벤트로는 아래 4개를 체크하고 생성한다
- Issue comments
- Pull request reviews
- Pushes
- Pull Requests
생성이 잘 되었다
그리고 repo명과 username 을 참고해서 USERNAME, REPO_ALLOWLIST 변수도 등록해준다
마지막으로 지금까지 등록한 변수들을 한번 확인하고 Atlantis 서버를 실행한다.
## 변수 설정 확인
echo $URL $USERNAME $TOKEN $SECRET $REPO_ALLOWLIST
## Atlantis 서버 실행
./atlantis server \
--atlantis-url="$URL" \
--gh-user="$USERNAME" \
--gh-token="$TOKEN" \
--gh-webhook-secret="$SECRET" \
--repo-allowlist="$REPO_ALLOWLIST"
실행이 성공한 것을 확인하고는 새 터미널을 켜서 ec2에 접속한 다음 atlantis가 4141 포트를 점유하고 있는지 확인한다
ss -tnlp
정상적으로 실행되고 있다면 등록한 URL을 크롬에 복붙해서 접속했을 때 아래와 같은 아틀란티스 화면이 뜬다
이쯤에서 아까만든 웹훅으로 돌아와보자. 웹훅을 생성하던 시점에는 Atlantis가 배포되지 않았으므로 502 에러가 떴었지만 이제 Atlantis 가 동작하고 있으므로 redeliver 은 성공적으로 수행된다
Atlantis GitOps 실습
이제 Atlantis는 준비되었으니 해당 repo에 테라폼 코드를 올려보자. (클론하는 레포는 위에서 생성한 내 레포여야 한다)
# git clone
git clone https://github.com/HitHereX/t101-cicd && cd t101-cicd && tree
# feature branch 생성
git branch test && git checkout test && git branch
# main.tf 파일 작성
echo 'resource "null_resource" "example" {}' > main.tf
# add commit push
git add main.tf && git commit -m "add main.tf" && git push origin test
++DEBUG: fatal: not a valid object name: 'main'
main 브랜치에 아무것도 없어서 발생하는 에러로 initial commit 을 해주면 해결된다고 했으나 커밋할 게 없었기 때문에 readme.md를 만들어 main에 commit 하고 나서 test branch 를 만들어 주는 것으로 해결했다
null resource 지만 어쨌든 브랜치에 커밋을 했으니 PR 시에 자동으로 배포되는지를 확인해보자.
main ← test 브랜치로 PR을 생성한다 (내용은 필요없음)
생성하면 자동으로 plan이 실행되고 lock이 걸린다
github comment로 atlantis apply 를 주면 apply 가 실행되어 변경된 테라폼 코드대로 리소스가 배포된다
(의외로) merge 여부와는 상관이 없었다.
왼쪽은 PR 후 apply 전, 오른쪽은 apply 후의 atlantis 화면이다.
PR과 함께 plan 이 자동으로 실행되고 tfstate 가 잠긴다(lock)
comment로 apply 하면 이 내용이 반영되는 것을 볼 수 있다
apply 를 눌러보면 이렇게 콘솔에서 plan, apply 한 내용이 나온다
다른 리소스들도 배포하는 데 유용하게 사용할 수 있을 것 같다
apply 후 merge 를 별도로 수행해야 한다는 점은 아쉽지만 팀 내 룰으로 해결할 수 있는 부분일 것 같다.
리소스 삭제
아래 코드는 cloudformation으로 배포한 atlantis 리소스를 삭제한다. 깃헙 레포랑 키도 잘 지우도록 하자
aws cloudformation delete-stack --stack-name t101
혼자라면 아마 절대 못 했을 내용인데 CloudNet 팀의 스터디로 많은 내용을 빠르게 받아들일 수 있었다!
항상 진심으로 감사드립니다
'EKS@Terraform' 카테고리의 다른 글
[Terraform@CloudNet] Terraform과 OpenTofu (0) | 2024.08.03 |
---|---|
[Terraform@CloudNet] EKS on Fargate by Terraform (0) | 2024.07.27 |
[Terraform@CloudNet] Terraform Module (0) | 2024.07.13 |
[Terraform@CloudNet] Terraform State (0) | 2024.07.07 |
[Terraform@CloudNet] Terraform Provider (0) | 2024.07.06 |