모듈(Module)
모듈은 테라폼에서 재사용 가능한 구성 요소입니다. 여러 리소스의 집합을 캡슐화하여 다른 테라폼 구성 파일에서 호출할 수 있게 합니다. 모듈을 사용하면 인프라 코드를 더 구조화되고 관리하기 쉽게 만들 수 있습니다. 마치 개발에서 사용되는 라이브러리, 모듈 개념과 비슷하다고 할 수 있습니다.
자식 모듈과 자식모듈을 호출하는 루트 모듈의 디렉터리로 구조입니다.
모듈 기본 작성 원칙
- 디렉터리 형식 제안: 모듈 디렉터리 이름은 terraform-<프로바이더 이름>-<모듈 이름> 형식을 사용합니다. 이 형식은 모듈이 테라폼용이고, 어떤 프로바이더의 리소스를 포함하며, 모듈의 이름을 명확히 알 수 있도록 합니다.
- 모듈화 가능한 구조로 작성: 테라폼 구성은 처음부터 모듈화를 염두에 두고 작성합니다. 단일 루트 모듈이라도 후에 다른 모듈이 호출할 수 있도록 구조화하고, 리소스 묶음을 논리적으로 그룹화합니다.
- 독립적인 모듈 관리: 각각의 모듈은 독립적으로 관리합니다. 리모트 모듈을 사용하지 않더라도, 하위 모듈을 루트 모듈의 하위 파일 시스템에 두기보다는 동일한 파일 시스템 레벨에 위치시키거나 별도의 모듈 전용 공간에서 관리합니다. 이렇게 하면 VCS를 통해 관리하기가 더 수월합니다.
- 공개된 테라폼 레지스트리 참고: 공개된 테라폼 레지스트리의 모듈을 참고하여 변수 처리, 반복문 적용, 조건에 따른 리소스 활성/비활성 등의 모범 사례를 학습하고, 자신의 상황에 맞게 적용합니다.
- 모듈 공유: 작성된 모듈을 공개 또는 비공개로 게시하여 팀이나 커뮤니티와 공유합니다. 이를 통해 모듈의 사용성을 높이고, 피드백을 받아 더 발전된 모듈을 구성할 수 있습니다.
이러한 원칙들을 통해 테라폼 모듈을 효과적으로 관리하고 재사용성을 높일 수 있습니다.
모듈 기본 실습
하나의 프로비저닝에서 패스워드를 여러번 구성해야 하는 경우, 모듈화 하여 재사용할 수 있도록 실습을 진행합니다.
1. 자식모듈 생성
먼저 랜덤 비밀번호를 생성하여 출력하는 자식 모듈을 생성합니다. random_pet 프로바이더를 이용하여 랜덤 패스워드를 생성하고, isDB 변수 값의 여부에 따라, 다르게 출력되도록 합니다.
mkdir -p 06-module-traning/modules/terraform-random-pwgen
cd 06-module-traning/modules/terraform-random-pwgen
touch main.tf variable.tf output.tf
# main.tf
resource "random_pet" "name" {
keepers = {
ami_id = timestamp()
}
}
resource "random_password" "password" {
length = var.isDB ? 16 : 10
special = var.isDB ? true : false
override_special = "!#$%*?"
}
# variable.tf
variable "isDB" {
type = bool
default = false
description = "패스워드 대상의 DB 여부"
}
# output.tf
output "id" {
value = random_pet.name.id
}
output "pw" {
value = nonsensitive(random_password.password.result)
}
2. 자식 모듈 동작 테스트
pwd
~/terraform/module/06-module-traning/modules/terraform-random-pwgen
terraform init && terraform plan
terraform apply -auto-approve -var=isDB=true
id = "organic-egret"
pw = "zNMETHuNI!BAJSoR
3. 루트 모듈 작성
다수의 리소스를 같은 목적으로 여러 번 반복해서 사용하려면 리소스 수만큼 반복해 구성 파일을 정의해야 하고 이름도 고유하게 설정해줘야 하는 부담이 있지만, 모듈을 활용하면 반복되는 리소스 묶음을 최소화할 수 있습니다.
mkdir -p 06-module-traning/06-01-basic
cd 06-module-traning/06-01-basic
touch main.tf
# main.tf
module "mypw1" {
source = "../modules/terraform-random-pwgen"
}
module "mypw2" {
source = "../modules/terraform-random-pwgen"
isDB = true
}
output "mypw1" {
value = module.mypw1
}
output "mypw2" {
value = module.mypw2
}
4. 루트 모듈 실행
자식 모듈을 호출해 반복 재사용하는 루트 모듈 결과를 확인합니다.
# 경로 확인
pwd
~/terraform/module/06-module-traning/06-01-basic
# 실행
terraform init && terraform plan && terraform apply -auto-approve
Apply complete! Resources: 2 added, 0 changed, 2 destroyed.
Outputs:
mypw1 = {
"id" = "cheerful-jaybird"
"pw" = "ktUvhJWskQ"
}
mypw2 = {
"id" = "flowing-deer"
"pw" = "PLOwYF*wh1fUt8Tm"
}
# tfstate에 모듈 정보 확인 : VSCODE에서 terraform.tfstate 파일 확인
cat terraform.tfstate | grep module
"module": "module.mypw1",
"module": "module.mypw1",
"module": "module.mypw2",
"module": "module.mypw2",
## 모듈로 묶여진 리소스는 module이라는 정의를 통해 단순하게 재활용하고 반복 사용할 수 있다.
## 모듈의 결과 참조 형식은 module.<모듈 이름>.<output 이름>으로 정의된다.
cat .terraform/modules/modules.json | jq
{
"Modules": [
{
"Key": "",
"Source": "",
"Dir": "."
},
{
"Key": "mypw1",
"Source": "../modules/terraform-random-pwgen",
"Dir": "../modules/terraform-random-pwgen"
},
{
"Key": "mypw2",
"Source": "../modules/terraform-random-pwgen",
"Dir": "../modules/terraform-random-pwgen"
}
]
}
Terraform Runner(Atlantis)
Atlantis는 Git 기반 워크플로우와 통합하여 pull request(PR) 기반의 인프라 변경 관리를 지원하여 Terraform을 자동화하는 오픈소스입니다. Terraform 코드를 pull request 하고 comment를 통해서 테라폼 CLI를 수행하여, 테라폼을 자동화 및 형상화하고 협업에 있어 도움을 줍니다. 또한 간단한 UI도 제공합니다.
실습환경 구성
Atlantis를 활용해서 Terraform 코드를 실습할 수 있는 환경을 구성합니다.
1. AWS EC2 생성(atlantis 서버 역할)
AWS EC2를 통해서 atlantis를 구성하고 동작시킬 를 생성합니다.
먼저, CloudFormation 스택을 이용해서 EC2를 배포하고 확인합니다.
# 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
# CloudFormation 스택 배포
MYKEYNAME=<각자 자신의 AWS EC2 서울 리전 Keypair 이름>
MYKEYNAME=hackjap
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
# [모니터링] CloudFormation 스택 상태
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
# EC2 공인 IP 확인
aws cloudformation describe-stacks --stack-name t101 --query 'Stacks[*].Outputs[0].OutputValue' --output text
생성이 완료되면, SSH로 접속하여 실습에 필요한 기본 정보들을 확인합니다.
- aws 자격증명
- 테라폼 버전
- git 버전
- atlantis 버전
# ubuntu EC2에 SSH 접속
ssh -i ~/.ssh/jdg-keypair.pem ubuntu@$(aws cloudformation describe-stacks --stack-name t101 --query 'Stacks[*].Outputs[0].OutputValue' --output text)
---------------------------
# 계정 확인
whoami
# aws 자격증명 설정 : (옵션) IAM profile로 설정 -> 단 이경우 tf 파일에 설정 필요
aws --version
aws configure
AWS Access Key ID [None]: ####
AWS Secret Access Key [None]: ####
Default region name [None]: ap-northeast-2
Default output format [None]:
# aws 자격증명 확인
aws s3 ls
# 테라폼 버전 확인
terraform version
#
git version
#
ls -l
./atlantis version
2. Git Repo 생성
altantis는 아래와 같이 다양한 Git 레포지토리를 지원합니다.
- GitHub
- GitHub app
- GitLab
- Gitea
- Bitbucket Cloud (bitbucket.org)
- Bitbucket Server (aka Stash)
- Azure DevOps
이번 실습에서는 가장 대중적인 Github를 사용합니다. 그리고 실습에서 사용할 Git Repo(private)를 생성합니다.
3. Git Token 생성
실습에서 사용할 Git Token(classic)도 생성합니다.
4. Webhook Secret 생성
이후 실습 과정에서 atlantis는 webhook 사용 시, 24길이 이상의 secret이 필요하다고 합니다.
Generate Random Strings and Numbers 사이트를 통해서 랜덤으로 webhook secret을 미리 생성해놓습니다.
4. webhook 생성
Repoistory의 설정의 Webhooks 탭에서 실습에서 사용할 webhook을 생성하고 아래와 같이 설정합니다.
- payload URL : http://<EC2공인IP>:4141/events
- Content type : application/json
- webhook trigger
- Issue comments
- Pull request reviews
- Pushes
- Pull requests
webhook이 생성되고 Resent Deliveries 탭을 확인해 보면 502 에러를 보여줍니다. 이는 아직 atlantis를 실행시키지 않아 webhook에서 설정한 Endpoint URL과의 통신이 되지 않기 때문입니다.
5. Altantis 실행
EC2 서버에 접속하여, 준비된 매개변수를 지정하여 Atlantis를 실행합니다.
#
URL="http://$(curl -s ipinfo.io/ip):4141"
USERNAME=gasida
TOKEN='<GIT_TOKEN>'
SECRET='<WEBHOOK_SECRET>'
REPO_ALLOWLIST="github.com/hackjap/t101-cicd"
# 변수 설정 확인
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"
# [신규 터미널] 기본 tcp 4141 포트 오픈
ss -tnlp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 *:4141 *:* users:(("atlantis",pid=2089,fd=7))
...
# 웹 접속 확인
URL="http://$(curl -s ipinfo.io/ip):4141"
echo $URL
http://54.180.250.116:4141
브라우저를 통해http://<EC2_공인 IP>:4141 atlantis 웹 접속도 확인합니다.
webhook delivery 탭에서 Redeliver 버튼을 눌러보면 이번에는 정상적인 응답 결과를 가져오는 것을 확인할 수 있습니다.
실습 1 : null 프로바이더
Atlantis를 이용해서 간단하게 null resource 테라폼 코드를 배포하고 확인합니다.
Local에서 생성한 레포지토리를 Clone 받아 test 브랜치를 생성하고, 테라폼 코드를 작성하여 푸시합니다.
# git clone
git clone https://github.com/gasida/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
github에 접속하여 pull request를 생성합니다.
위에서 설정한 webhook trigger에 의해서 pull request시, webhook이 수행되어 Plan이 자동 수행되는 것을 확인할 수 있습니다.
다음과 같이 comment 기능을 통해서 명령어를 수행시킬 수 있습니다.
보안상의 이유로 아무 명령어나 실행되면 안 되기 때문에, 기본적으로 설정된 명령어를 제외하고는 명령어 실행 시 , 아래와 같이 에러가 발생하는 것을 확인할 수 있습니다.
Add a Command로 apply를 하고 결과를 확인합니다.
실제로 서버에 접속해 보면. atlantis 경로 아래에 terraform 파일들이 생성됨을 확인할 수 있습니다.
apply과 완료되면 Pull request를 Confirm Merge 합니다.
서버의 파일을 확인해 보면 테라폼 관련 파일들이 제거됨을 확인할 수 있습니다.
atlantis 웹에서도 수행된 기록과 결과를 확인할 수 있습니다.
실습 2 : aws iam user 생성 + Terraform Backend(S3)
이번에는 atlantis를 이용하여 aws iam user를 생성해 봅니다. 추가적으로 Terraform Backend를 통해서 원격으로 State 파일을 관리합니다.
1. AWS S3 버킷 생성
Terraform Backend State 저장용도로 사용할 S3 버킷을 생성합니다.
#
aws s3 ls
#
aws s3 mb s3://<각자 유일한 S3 버킷명 이름> --region ap-northeast-2
aws s3 mb s3://hackjap-t101 --region ap-northeast-2
#
aws s3 ls
2024-07-14 07:05:23 hackjap-t101
2. Local에서 Git 코드 작업
이번에는 iam 브랜치를 생성하고 테라폼 코드를 작성하여 해당 브랜치에 커밋 & 푸시합니다.
# feature branch 생성
git branch iam && git checkout iam && git branch
# 디렉터리 생성
mkdir iam && cd iam
# main.tf 파일 작성
vi main.tf
----------
terraform {
backend "s3" {
bucket = "<각자 자신의 S3 버킷 이름>"
key = "terraform.tfstate"
region = "ap-northeast-2"
}
}
resource "aws_iam_user" "myuser" {
name = "t101user"
}
----------
terraform {
backend "s3" {
bucket = "hackjap-t101"
key = "terraform.tfstate"
region = "ap-northeast-2"
}
}
resource "aws_iam_user" "myuser" {
name = "t101user"
}
----------
# add commit push
git add main.tf && git commit -m "add main.tf" && git push origin iam
3. Atlantis 확인
Github에 접속하여 pull request를 생성합니다,.
pull request가 생성되면 terraform plan이 되고, 서버에도 브랜치명으로 폴더가 구분되어 terraform plan 수행 결과 파일을 확인할 수 있습니다.
Add a comment로 apply를 수행하고 결과를 확인합니다.
apply가 잘 수행됨에 따라 Terraform Backend를 통해서 원격으로 State 파일이 잘 관리됨을 확인할 수 있습니다.
마지막으로, confirm merge 버튼을 눌러 pull requset를 merge 합니다.
실습 3 : 실습 2에서 생성한 리소스 삭제
Add a comment를 통해 destroy를 수행하면 에러가 발생한다. 이는 atlantis는 삭제 기능이 따로 존재하지 않기 때문이다. 또한 실습 2에서는 Terraform backend를 통해 tfstate파일 따로 관리하기 때문에, 추가적으로 feature 브랜치를 생성하여 삭제를 위한 코드 작업이 필요하다.
1. Local에서 Git 코드 작업
deleteiam 브랜치를 생성하고 삭제를 위한 main.tf 파일을 작성하고 커밋 & 푸시한다. 삭제만을 위한 작업이기 때문에, 추가적은 코드를 제외하고 Backend만 지정해 준다.(tfstate파일)
# feature branch 생성
git branch deleteiam && git checkout deleteiam && git branch
# 디렉터리 생성
mkdir deleteiam && cd deleteiam
# main.tf 파일 작성
vi main.tf
----------
terraform {
backend "s3" {
bucket = "<각자 자신의 S3 버킷 이름>"
key = "terraform.tfstate"
region = "ap-northeast-2"
}
}
----------
terraform {
backend "s3" {
bucket = "hackjap-t101"
key = "terraform.tfstate"
region = "ap-northeast-2"
}
}
----------
# add commit push
git add main.tf && git commit -m "add main.tf" && git push origin deleteiam
2. atlantis 확인
pull request를 생성하고 plan 결과를 확인해 보면 삭제가 될 예정임을 확인할 수 있습니다.
Add a comment로 apply 하면 리소스가(iam) 정상적으로 잘 삭제됨을 확인할 수 있습니다.
실습 자원 삭제
1. AWS 리소스 삭제
# cloudformation
aws cloudformation delete-stack --stack-name t101
# s3
aws s3 rm s3://hackjap-t101 --recursive
aws s3 rb s3://hackjap-t101
2. git 리소스 삭제
Github repo 삭제
Repo → Settings → Delete this repository
Github token 삭제
Settings → Developer Settings -> Personal access tokens -> Delete this repository
Local 코드 삭제
rm -r ~/hackjap-t101
'Automation > Terraform' 카테고리의 다른 글
Terraform OpenTofu - T101 9주차 (0) | 2024.08.03 |
---|---|
테라폼으로 AWS EKS 배포 - T101 7주차 (0) | 2024.07.27 |
Terraform Provider & State - T101 4주차 (0) | 2024.07.06 |
Terraform 기본사용 3 - T101 3주차 (0) | 2024.06.29 |
Terraform 기본사용 2 - T101 2주차 (0) | 2024.06.23 |