IaC와 테라폼
IaC(Infrastructure as Code)
프로그래밍에서와 유사하게 코드를 이용하여 인프라 리소스를 정의하고, 조합하는 형태로 관리한다.
수작업으로 하는 것보다 안정성, 일관성, 재현 가능성을 향상시킬 수 있다.
테라폼
Hashicorp 사에서 제공하는 IaC 도구이다.
아래 링크에서 더 많은 내용을 확인할 수 있다.
https://developer.hashicorp.com/terraform
실습
1. 아래 링크에서 테라폼을 설치한다.
https://developer.hashicorp.com/terraform/install
2. main.tf 파일을 아래와 같이 작성한다.
provider를 docker로 설정하고, required_providers의 docker의 source로 테라폼 레지스트리에서 설정을 가져온다.
resource에서 도커 이미지를 nginx로 지정하고, http에 해당하는 포트(80)를 열어서 이걸 8000 포트로 노출한다.
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.1"
}
}
}
provider "docker" {}
resource "docker_image" "nginx" {
name = "nginx"
keep_locally = false
}
resource "docker_container" "nginx" {
image = docker_image.nginx.image_id
name = "sample"
ports {
internal = 80
external = 8000
}
}
3. 테라폼 초기화를 진행한다.
terraform init
4. 파일의 형태를 통일하기 위해서 포맷팅한다.
terraform fmt
5. 리소스를 생성한다.
Enter a value라는 라인이 출력되면 yes를 입력하고 엔터를 누른다.
terraform apply
6. 현재 인프라의 상태를 확인한다.
terraform show
7. 아래 명령어를 실행하여 sample이라는 컨테이너를 확인한다.
docker ps
8. 해당 컨테이너로 접속해본다.
curl localhost:8000
이제 변수를 이용해서 인프라를 구성해보자.
1. main.tf 파일을 작성한다.
아까와 동일한 파일이지만 도커 컨테이너 리소스의 name 부분과 external 포트 번호가 변경되었다.
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.1"
}
}
}
provider "docker" {}
resource "docker_image" "nginx" {
name = "nginx"
keep_locally = false
}
resource "docker_container" "nginx" {
image = docker_image.nginx.image_id
name = var.container_name
ports {
internal = 80
external = 9000
}
}
2. variables.tf 파일을 작성한다.
variable "container_name" {
description = "Value of the name for the Docker container"
type = string
default = "ExampleNginxContainer"
}
3. 아래 명령을 실행한다.
이제 Enter a value라는 라인이 출력되지 않는다.
terraform apply --auto-approve -var "container_name=YetAnotherName"
4. 아래 명령을 실행하면 NAMES가 위에서 설정한 YetAnotherName으로 설정된 것을 확인할 수 있다.
docker ps
쿠버네티스 + 테라폼 + 젠킨스
k8s 클러스터를 테라폼으로 제어
1. 새로운 빈 폴더를 만들고 kubernetes.tf 파일을 생성한다.
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
variable "host" {
type = string
}
variable "client_certificate" {
type = string
}
variable "client_key" {
type = string
}
variable "cluster_ca_certificate" {
type = string
}
provider "kubernetes" {
host = var.host
client_certificate = base64decode(var.client_certificate)
client_key = base64decode(var.client_key)
cluster_ca_certificate = base64decode(var.cluster_ca_certificate)
}
2. 다음 명령을 실행한다.
실행 결과에서 client-certificate-data, client-key-data, cluster-authority-data를 복사해둔다.
kubectl config view --minify --flatten
3. terraform.tfvars를 작성한다.
host = "https://127.0.0.1:6443"
client_certificate = "clinet-certificate-data 값"
client_key = "client-key-data 값"
cluster_ca_certificate = "client-authority-data 값"
4. deployment.tf를 작성한다.
resource "kubernetes_deployment" "nginx" {
metadata {
name = "scalable-nginx-example"
labels = {
App = "ScalableNginxExample"
}
}
spec {
replicas = 4
selector {
match_labels = {
App = "ScalableNginxExample"
}
}
template {
metadata {
labels = {
App = "ScalableNginxExample"
}
}
spec {
container {
image = "nginx:1.7.8"
name = "example"
port {
container_port = 80
}
}
}
}
}
}
5. service.tf 파일을 작성한다.
resource "kubernetes_service" "nginx" {
metadata {
name = "nginx-example"
}
spec {
selector = {
App = kubernetes_deployment.nginx.spec.0.template.0.metadata[0].labels.App
}
port {
node_port = 30201
port = 80
target_port = 80
}
type = "NodePort"
}
}
6. 테라폼 초기화를 수행하고, 리소스를 생성한다.
terraform init
terraform apply --auto-approve
7. 해당 포트로 요청을 보내면 IaC와 테라폼에서 수행했던 결과와 동일한 nginx의 html을 확인할 수 있다.
curl localhost:30201
8. 환경을 정리한다.
terraform destroy
Terraform Cloud
깃허브와 같은 SCM에 인프라 상태 정보를 저장하는 것과 k8s pv를 사용하는 방법은 인프라 상태 정보를 유지하는데 적절하지 않다. 그래서 보통 vault 서비스를 이용한다.
1. https://app/terraform.io에서 Terraform Cloud 계정을 생성한다.
2. User Settings > Organization에서 Organization을 새로 생성한다. 이름은 PRGMS_COURSE_CICD로 설정한다.
3. 터미널에 다음 명령어를 입력하여 로그인한다.
리다이렉트 된 웹 화면에서 토큰을 생성하고, 이 토큰으로 로그인을 한다.
terraform login
4. Projects & workspaces에서 새로운 워크스페이스를 생성하고, 생성된 워크스페이스의 설정에서 Execution Mode를 Custom Local로 설정한다.
5. nginx.tf 파일을 생성한다.
terraform {
cloud {
organization = "PRGMS_COURSE_CICD"
workspaces {
name = "nginx-example"
}
}
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
variable "kubernetes_config_path" {
default = "~/.kube/config"
}
variable "kubernetes_namespace" {
default = "default"
}
provider "kubernetes" {
config_path = var.kubernetes_config_path
}
resource "kubernetes_deployment" "nginx" {
metadata {
name = "nginx-example"
labels = {
App = "NginxExample"
}
namespace = var.kubernetes_namespace
}
spec {
replicas = 4
selector {
match_labels = {
App = "NginxExample"
}
}
template {
metadata {
labels = {
App = "NginxExample"
}
}
spec {
container {
image = "nginx:1.7.8"
name = "example"
port {
container_port = 80
}
}
}
}
}
}
resource "kubernetes_service" "nginx" {
metadata {
name = "nginx-example"
namespace = var.kubernetes_namespace
}
spec {
selector = {
App = kubernetes_deployment.nginx.spec.0.template.0.metadata[0].labels.App
}
port {
node_port = 30201
port = 80
target_port = 80
}
type = "NodePort"
}
}
6. 테라폼을 초기화하고, 리소스를 생성한다.
terraform init
terraform apply --auto-approve
7. nginx-example이라는 디플로이먼트와 서비스가 생성되었는지 확인한다.
kubectl get deployments
kubectl get services
젠킨스 파이프라인 테스트 준비
1. 깃허브에 새로운 레포지토리를 생성하고, 위에서 사용했던 nginx.tf 파일을 넣는다.
2. 젠킨스 credentials에 테라폼 클라우드 인증 정보를 추가한다.
이 credentials의 id는 'terraform-credentials'이며, secret에는 위의 3에서 만들었던 토큰을 입력한다.
3. 젠킨스 플러그인에서 Terraform Plugin을 설치한다.
4. 젠킨스 설정의 Tools에서 Terraform installations에서 1.6.6 버전인 해당하는 운영체제에 맞는 버전을 선택한다.
5. 파이프라인 스크립트를 다음과 같이 작성하고, 빌드를 수행한다.
pipeline {
agent any
tools {
terraform 'terraform'
}
stages {
stage('Checkout') {
steps {
git url: 'git@github.com:ncherryu/terraform.git',
branch: 'main',
credentialsId: 'github-credentials'
}
}
stage('Terraform init') {
steps {
withCredentials([string(credentialsId: 'terraform-credentials',
variable: 'TERRAFORM_TOKEN')]) {
sh'''
export TF_TOKEN_app_terraform_io="${TERRAFORM_TOKEN}"
terraform init
'''
}
}
}
stage('Terraform apply') {
steps {
withCredentials([file(credentialsId: 'KUBECONFIG',
variable: 'KUBECONFIG_PATH'),
string(credentialsId: 'terraform-credentials',
variable: 'TERRAFORM_TOKEN')]) {
sh'''
export TF_TOKEN_app_terraform_io="${TERRAFORM_TOKEN}"
terraform validate
terraform apply \
-var "kubernetes_config_path=${KUBECONFIG_PATH}" \
--auto-approve
'''
}
}
}
}
}
CD 파이프라인
빌드 버전 관리
하나의 클러스터 내에서 네임스페이스만 별도로 구성하고, production 환경과 staging 환경이 분리된 상황에서의 파이프라인 구성을 시뮬레이션해보자.
1. 위에서 생성한 Organization인 PRGMS_COURSE_CICD에서 두 워크스페이스를 생성한다.
하나는 스테이징 워크스페이스인 calculator-dev이고, 다른 하나는 프로덕션 워크스페이스인 calculator-prod이다.
Execution Mode는 로컬로 설정한다.
2. production.conf 파일을 다음과 같이 작성한다.
terraform {
cloud {
organization = "PRGMS_COURSE_CICD"
workspaces {
name = "calculator-prod"
}
}
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
locals {
deployment = {
namespace = "prod"
service_port = 31000
}
}
3. staging.conf 파일을 다음과 같이 작성한다.
2와 비교했을 때 이용하는 테라폼 클라우드의 워크스페이스가 다르고, namespace와 포트 번호도 다르다.
terraform {
cloud {
organization = "PRGMS_COURSE_CICD"
workspaces {
name = "calculator-dev"
}
}
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
locals {
deployment = {
namespace = "dev"
service_port = 32000
}
}
4. resources.conf 파일을 작성한다.
- kubernetes_config_path에서는 서로 다른 kubeconfig를 적용하기 위한 변수를 설정한다.
- calculator_img에서는 포드 생성에 적용할 이미지를 변수로 설정한다. 빌드 버전이 달라지면 서로 다른 이미지 태그를 적용하기 위함이다.
- kubernetes_namespace ns에서는 앞에서 지정한 k8s 네임스페이스(prod, dev)를 리소스로 생성한다. 테라폼 디스트로이를 하면 없어진다.
variable "kubernetes_config_path" {
default = "~/.kube/config"
}
variable "calculator_img" {
default = ""
}
provider "kubernetes" {
config_path = var.kubernetes_config_path
}
resource "kubernetes_namespace" "ns" {
metadata {
name = local.deployment.namespace
}
}
resource "kubernetes_deployment" "calculator" {
metadata {
name = "calculator"
labels = {
App = "Calculator"
}
namespace = local.deployment.namespace
}
spec {
replicas = 3
selector {
match_labels = {
App = "Calculator"
}
}
template {
metadata {
labels = {
App = "Calculator"
}
}
spec {
container {
image = var.calculator_img
name = "calculator"
port {
container_port = 8080
}
}
}
}
}
}
resource "kubernetes_service" "calculator" {
metadata {
name = "calculator"
namespace = local.deployment.namespace
}
spec {
selector = {
App = kubernetes_deployment.calculator.spec.0.template.0.metadata[0].labels.App
}
port {
node_port = local.deployment.service_port
port = 8080
target_port = 8080
}
type = "NodePort"
}
}
5. 젠킨스에서 build timestamp 플러그인을 설치하고, 젠킨스 설정의 시스템에서 타임존을 서울로 설정하고, 패턴은 'yyyyMMdd-HHmm'으로 설정한다.
6. 아래 명령으로 staging.conf 파일과 resources.conf 파일을 calculator.tf 파일에 복사한다.
윈도우 운영체제를 사용하고 있기 때문에 cat 대신 type을 사용한다.
type staging.conf resources.conf > calculator.conf
7. 테라폼을 초기화하고, 리소스를 생성한다.
terraform init
terraform apply -var "calculator_img=localhost:30100/calculator:1.0"
8. 제대로 생성되었는지 확인한다.
kubectl get namespaces
kubectl get deployments -n dev
젠킨스 파이프라인
1. Dockerfile을 다음과 같이 작성하고, build & push 해서 레지스트리에 등록한다.
FROM jenkins/jnlp-agent-docker
USER root
COPY entrypoint.sh /entrypoint.sh
RUN chown jenkins:jenkins /entrypoint.sh
RUN chmod +x /entrypoint.sh
RUN wget https://releases.hashicorp.com/terraform/1.6.6/terraform_1.6.6_windows_amd64.zip
RUN unzip terraform_1.6.6_windows_amd64.zip
RUN mv terraform /bin && chmod +x /bin/terraform
USER jenkins
ENTRYPOINT ["/entrypoint.sh"]
docker build -t ncherryu/jnlp-terraform
docker push ncherryu/jnlp-terraform
2. 파이프라인 스크립트를 다음과 같이 작성하고, 빌드하여 결과를 확인한다.
pipeline {
agent {
kubernetes {
yaml'''
aliVersion: v1
kind: Pod
spec:
containers:
- name: jnlp
image: ncherryu/jnlp-terraform
env:
- name: DOCKER_HOST
value: "tcp://localhost:2375"
- name: dind
image: ncherryu/dind
command:
- /usr/local/bin/dockerd-entrypoint.sh
env:
- name: DOCKER_TLS_CERTDIR
value: ""
securityContext:
privileged: true
- name: builder
image: ncherryu/jenkins-agent-jdk-17
command:
- cat
tty: true
'''
}
}
environment {
REGISTRY_URI = 'registry-service.registry.svc.cluster.local:30100'
VERSION_TAG = '${BUILD_TIMESTAMP}'
STAGING_URL = 'http://calculator.dev.svc.cluster.local:8080'
PRODUCTION_URL = 'http://calculator.prod.svc.cluster.local:8080'
}
stages {
stage("Checkout") {
steps {
git url: 'git@github.com:ncherryu/calculator.git',
branch: 'master',
credentialsId: 'github-credentials'
}
}
stage("Compile") {
steps {
script {
container('builder') {
sh "./gradlew compileJava"
}
}
}
}
stage("Unit Test") {
steps {
script {
container('builder') {
sh "./gradlew test"
publishHTML(target: [
reportDir: 'build/reports/tests/test',
reportFiles: 'index.html',
reportName: 'JUnit Report'
])
}
}
}
}
stage("Code Coverage") {
steps {
script {
container('builder') {
sh "./gradlew jacocoTestReport"
publishHTML(target: [
reportDir: 'build/reports/jacoco/test/html',
reportFiles: 'index.html',
reportName: 'JaCoCo Report'
])
sh "./gradlew jacocoTestCoverageVerification"
}
}
}
}
stage("Static Analysis") {
steps {
script {
container('builder') {
sh "./gradlew checkstyleMain"
publishHTML(target: [
reportDir: 'build/reports/checkstyle',
reportFiles: 'main.html',
reportName: 'Checkstyle Report'
])
}
}
}
}
stage("Package") {
steps {
script {
container('builder') {
sh "./gradlew build"
}
}
}
}
stage("Docker Build") {
steps {
script {
dockerImage = docker.build "calculator"
}
}
}
stage("Docker Push") {
steps {
script {
docker.withRegistry("https://${REGISTRY_URI}") {
dockerImage.push("${VERSION_TAG}")
}
}
}
}
stage("Deploy to Staging") {
steps {
withCredentials([file(credentialsId: 'KUBECONFIG',
variable: 'KUBECONFIG_PATH'),
string(credentialsId: 'terraform-credentials',
variable: 'TF_TOKEN_app_terraform_io')]) {
sh'''
cd iac
cat staging.conf resources.conf > calculator.tf
terraform init
terraform apply --auto approve \
-var "kubernetes_config_path=${KUBECONFIG_PATH}" \
-var "calculator_img=localhost:30100/calculator;${VERSION_TAG}"
'''
}
}
}
stage("Acceptance Test") {
steps {
script {
container('builder') {
sleep 60
sh "./gradlew acceptanceTest " +
"-Dcalculator.url=${STARTING_URL}"
publishHTML(target: [
reportDir: 'build/reports/tests/acceptanceTest',
reportFiles: 'index.html',
reportName: 'Acceptance Test Report'
])
}
}
}
}
stage("Release") {
steps {
withCredentials([file(credentialsId: 'KUBECONFIG',
variable: 'KUBECONFIG_PATH'),
string(credentialsId: 'terraform-credentials',
variable: 'TF_TOKEN_app_terraform_io')]) {
sh'''
cd iac
cat production.conf resources.conf > calculator.tf
terraform init
terraform apply --auto approve \
-var "kubernetes_config_path=${KUBECONFIG_PATH}" \
-var "calculator_img=localhost:30100/calculator;${VERSION_TAG}"
'''
}
}
}
stage("Smoke Test") {
steps {
script {
container('builder') {
sleep 60
sh "./gradlew acceptanceTest " +
"-Dcalculator .url=${PRODUCTION_URL}"
}
}
}
}
post {
always {
withCredentials([file(credentialsId: 'KUBECONFIG',
variable: 'KUBECONFIG_PATH'),
string(credentialsId: 'terraform-credentials',
variable: 'TF_TOKEN_app_terraform_io')]) {
sh'''
cd iac
cat staging.conf resources.conf > calculator.tf
terraform init
terraform apply --auto approve \
-var "kubernetes_config_path=${KUBECONFIG_PATH}"
'''
}
}
}
}
}
배운 점
- 테라폼으로 인프라를 구성하는 방법을 배웠다.
- 쿠버네티스 클러스터를 테라폼으로 제어하는 하는 법을 알게 되었다.
- 테라폼 클라우드를 이용하여 젠킨스 파이프라인 테스트를 하는 방법을 배웠다.