Github Action 发布Terraform基础设施项目
一 背景
GitHub Action 集成 Terraform 的方案背景主要有以下几个方面:
- 基础设施即代码:随着云计算技术的发展,基础设施即代码成为了管理云基础设施和应用程序的主流方式。使用基础设施即代码工具,如 Terraform,可以帮助开发者在云端环境中管理基础设施和应用程序,实现自动化部署和流程简化。
- DevOps 原则:DevOps 是一种软件开发和运维的方法论,强调开发和运维之间的协作和共同责任,以提高软件交付速度和质量。GitHub Actions 集成 Terraform 可以帮助实现 DevOps 原则,提高开发效率和质量,缩短上线时间。
- 自动化部署:自动化部署是 DevOps 的核心要素之一,可以减少手动干预,提高效率和准确性。使用 GitHub Actions 集成 Terraform,可以实现自动化部署,提高部署的效率和可靠性。
- 与 GitHub 集成:GitHub 是一个广泛使用的代码托管平台,提供了许多与代码管理和协作相关的功能。通过 GitHub Actions 集成 Terraform,可以直接与 GitHub 仓库进行集成,帮助开发者更好地管理代码和部署过程。
综上所述,GitHub Actions 集成 Terraform 方案的背景主要是基础设施即代码、DevOps 原则、自动化部署和与 GitHub 集成等方面的需求和趋势。这种方案可以帮助开发者更好地管理基础设施和应用程序,提高开发效率和质量,缩短上线时间。
二 集成方案特点
GitHub Actions 集成 Terraform 方案的优缺点如下:
2.1 优点
- 自动化部署:通过 GitHub Actions 集成 Terraform,可以实现自动化部署,减少手动干预,提高效率和准确性。
- 简化流程:使用 Terraform 管理云基础设施和应用程序可以简化流程,提高开发和部署的效率,缩短上线时间。
- 可靠性高:Terraform 已经被广泛使用和验证,可以帮助开发者在云端环境中管理基础设施和应用程序,提高可靠性和稳定性。
- 与 GitHub 集成:由于 GitHub Actions 集成 Terraform,可以直接与 GitHub 仓库进行集成,帮助开发者更好地管理代码和部署过程。
2.2 缺点
- 学习成本:使用 Terraform 和 GitHub Actions 集成需要一定的学习成本,需要掌握 Terraform 和 GitHub Actions 的基本知识和技能。
- 复杂性:Terraform 作为一种基础设施即代码工具,需要开发者对基础设施和云服务有一定的了解和认知,否则可能会增加复杂度和风险。
- 安全问题:在使用 GitHub Actions 和 Terraform 集成时,需要注意安全问题,如如何存储和保护访问密钥等敏感信息,否则可能会导致数据泄露和安全问题。
- 成本问题:使用 Terraform 和云服务时,需要考虑成本问题,如如何优化云资源的使用和控制成本等,否则可能会增加开销和费用。
综合来看,GitHub Actions 集成 Terraform 方案具有许多优点,可以帮助开发者实现自动化部署和流程简化,提高开发效率和可靠性。但是,也需要注意学习成本、复杂性、安全问题和成本问题等方面的注意事项。
三 实战
3.1 先决条件
- 使用github进行代码托管包含terraform和github action工作流;
- 使用github action进行terraform集成设施变更;
- 由于使用流水线,terraform state 文件需要放在项目外,利用terraform cloud进行存储remote state;
3.2 代码
- 代码结构
.
├── .github
│ └── workflows
│ └── terraform.yml
├── .gitignore
├── .terraform.lock.hcl
├── README.md
├── header.tf
├── key
│ └── .ssh
│ ├── id_rsa
│ └── id_rsa.pub
├── main.tf
├── outputs.tf
├── user_data.tpl
├── userdata_centos.tpl
└── variables.tf
- tf代码
variable "database_name" {
type = string
default = "xuel_wp"
}
variable "database_password" {
type = string
default = "WWW.51idc.com"
}
variable "database_user" {
type = string
default = "wpuser"
}
variable "region" {
type = string
default = "cn-north-1"
}
# variable "shared_credentials_file" {}
variable "IsCentos" {
type = bool
default = false
}
variable "public_subnet_cidrs" {
type = list(string)
description = "Public Subnet CIDR values"
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
variable "private_subnet_cidrs" {
type = list(string)
description = "Private Subnet CIDR values"
default = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
}
variable "AZ1" {
type = string
default = "cn-north-1a"
}
variable "AZ2" {
type = string
default = "cn-north-1b"
}
variable "AZ3" {
type = string
default = "cn-north-1d"
}
variable "VPC_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "subnet1_cidr" {
type = string
default = "10.0.1.0/24"
}
variable "subnet2_cidr" {
type = string
default = "10.0.2.0/24"
}
variable "subnet3_cidr" {
type = string
default = "10.0.3.0/24"
}
# variable "instance_type" {}
variable "instance_class" {
type = string
default = "db.t3.micro"
}
variable "PUBLIC_KEY_PATH" {
type = string
default = "./key/.ssh/id_rsa.pub"
}
variable "PRIV_KEY_PATH" {
type = string
default = "./key/.ssh/id_rsa"
}
variable "root_volume_size" {
type = number
default = 20
}
variable "access_key" {
}
variable "secret_key" {
}
terraform {
cloud {
organization = "devopsxuel"
workspaces {
name = "terraform-gh-aws-wordpress"
}
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~>4.30.0"
}
null = {
version = "~> 3.1.1"
}
template = {
version = "~> 2.2.0"
}
}
required_version = ">= 1.2.0"
}
provider "aws" {
region = var.region
access_key = var.access_key
secret_key = var.secret_key
}
locals {
xuel_tag = {
application = "xuel_app_test"
environment = "dev"
purpose = "person test"
contact = "xuel@anchnet.com"
creator = "xuelei"
role = "cloud manager"
owner = "xuel"
project = "aws_partner_project"
team = "smartops"
organization = "mse"
company = "anchnet"
}
}
#####################################################################
# Data
#####################################################################
data "aws_ami" "linux2" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
data "aws_ami" "centos" {
most_recent = true
filter {
name = "name"
values = ["CentOS-7-9-1210-updated-BJ"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
# owners = ["aws-marketplace"]
}
data "aws_ec2_instance_type_offering" "instance_type" {
filter {
name = "instance-type"
values = ["t2.micro", "t3.micro"]
}
preferred_instance_types = ["t3.micro", "t2.micro"]
}
#####################################################################
# Network
#####################################################################
resource "aws_vpc" "prod-vpc" {
cidr_block = var.VPC_cidr
enable_dns_support = "true" #gives you an internal domain name
enable_dns_hostnames = "true" #gives you an internal host name
instance_tenancy = "default"
}
# Create Public Subnet for EC2
resource "aws_subnet" "prod-subnet-public-1" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet1_cidr
map_public_ip_on_launch = "true" //it makes this a public subnet
availability_zone = var.AZ1
}
# Create Private subnet for RDS
resource "aws_subnet" "prod-subnet-private-1" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet2_cidr
map_public_ip_on_launch = "false" //it makes private subnet
availability_zone = var.AZ2
}
# Create second Private subnet for RDS
resource "aws_subnet" "prod-subnet-private-2" {
vpc_id = aws_vpc.prod-vpc.id
cidr_block = var.subnet3_cidr
map_public_ip_on_launch = "false" //it makes private subnet
availability_zone = var.AZ3
}
# Create IGW for internet connection
resource "aws_internet_gateway" "prod-igw" {
vpc_id = aws_vpc.prod-vpc.id
}
# Creating Route table
resource "aws_route_table" "prod-public-crt" {
vpc_id = aws_vpc.prod-vpc.id
route {
//associated subnet can reach everywhere
cidr_block = "0.0.0.0/0"
//CRT uses this IGW to reach internet
gateway_id = aws_internet_gateway.prod-igw.id
}
}
# Associating route tabe to public subnet
resource "aws_route_table_association" "prod-crta-public-subnet-1" {
subnet_id = aws_subnet.prod-subnet-public-1.id
route_table_id = aws_route_table.prod-public-crt.id
}
#####################################################################
# Sg
#####################################################################
//security group for EC2
resource "aws_security_group" "ec2_allow_rule" {
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTP"
from_port = 8888
to_port = 8888
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "MYSQL"
from_port = 3306
to_port = 3306
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
vpc_id = aws_vpc.prod-vpc.id
tags = local.xuel_tag
}
# Security group for RDS
resource "aws_security_group" "RDS_allow_rule" {
vpc_id = aws_vpc.prod-vpc.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = ["${aws_security_group.ec2_allow_rule.id}"]
}
# Allow all outbound traffic.
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.xuel_tag
}
# Create RDS Subnet group
resource "aws_db_subnet_group" "RDS_subnet_grp" {
subnet_ids = ["${aws_subnet.prod-subnet-private-1.id}", "${aws_subnet.prod-subnet-private-2.id}"]
}
#####################################################################
# RDS
#####################################################################
# Create RDS instance
resource "aws_db_instance" "wordpressdb" {
allocated_storage = 10
engine = "mysql"
engine_version = "5.7"
instance_class = var.instance_class
db_subnet_group_name = aws_db_subnet_group.RDS_subnet_grp.id
vpc_security_group_ids = ["${aws_security_group.RDS_allow_rule.id}"]
db_name = var.database_name
username = var.database_user
password = var.database_password
skip_final_snapshot = true
# make sure rds manual password chnages is ignored
lifecycle {
ignore_changes = [password]
}
}
#####################################################################
# template
#####################################################################
# change USERDATA varible value after grabbing RDS endpoint info
data "template_file" "user_data" {
template = var.IsCentos ? file("${path.module}/userdata_centos.tpl") : file("${path.module}/user_data.tpl")
vars = {
db_username = var.database_user
db_user_password = var.database_password
db_name = var.database_name
db_RDS = aws_db_instance.wordpressdb.endpoint
}
}
#####################################################################
# EC2
#####################################################################
# Create EC2 ( only after RDS is provisioned)
resource "aws_instance" "wordpressec2" {
ami = var.IsCentos ? data.aws_ami.centos.id : data.aws_ami.linux2.id
instance_type = data.aws_ec2_instance_type_offering.instance_type.id
subnet_id = aws_subnet.prod-subnet-public-1.id
vpc_security_group_ids = ["${aws_security_group.ec2_allow_rule.id}"]
user_data = data.template_file.user_data.rendered
key_name = aws_key_pair.mykey-pair.id
tags = local.xuel_tag
root_block_device {
volume_size = var.root_volume_size # in GB
}
# this will stop creating EC2 before RDS is provisioned
depends_on = [aws_db_instance.wordpressdb]
}
// Sends your public key to the instance
resource "aws_key_pair" "mykey-pair" {
key_name = "mykey-pair"
public_key = file(var.PUBLIC_KEY_PATH)
}
# creating Elastic IP for EC2
resource "aws_eip" "eip" {
instance = aws_instance.wordpressec2.id
}
#####################################################################
# exec
#####################################################################
resource "null_resource" "Wordpress_Installation_Waiting" {
# trigger will create new null-resource if ec2 id or rds is chnaged
triggers = {
ec2_id = aws_instance.wordpressec2.id,
rds_endpoint = aws_db_instance.wordpressdb.endpoint
}
connection {
type = "ssh"
user = var.IsCentos ? "root" : "ec2-user"
private_key = file(var.PRIV_KEY_PATH)
host = aws_eip.eip.public_ip
}
provisioner "remote-exec" {
inline = ["ls"]
}
}
output "IP" {
value = aws_eip.eip.public_ip
}
output "RDS-Endpoint" {
value = aws_db_instance.wordpressdb.endpoint
}
output "INFO" {
value = "AWS Resources and WordPress has been provisioned. Go to http://${aws_eip.eip.public_ip}"
}
3.3 Github Action
name: "Terraform Infrastructure Change Management Pipeline with GitHub Actions"
on:
push:
branches:
- main
pull_request: {}
env:
# verbosity setting for Terraform logs
TF_LOG: INFO
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
jobs:
terraform:
name: "Terraform Infrastructure Change Management"
runs-on: ubuntu-latest
defaults:
run:
shell: bash
# We keep Terraform files in the terraform directory.
working-directory: .
steps:
- name: Checkout the repository to the runner
uses: actions/checkout@v2
- name: Setup Terraform with specified version on the runner
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.3.0
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Terraform format
id: fmt
run: terraform fmt -check || echo true
- name: Terraform init
id: init
run: terraform init
- name: Terraform validate
id: validate
run: terraform validate
- name: Terraform plan
id: plan
if: github.event_name == 'push'
run: terraform plan -no-color -input=false
continue-on-error: true
# - uses: actions/github-script@v6
# if: github.event_name == 'push'
# env:
# PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
# with:
# script: |
# const output = `#### Terraform Format and Style ?\`${{ steps.fmt.outcome }}\`
# #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
# #### Terraform Validation ?\`${{ steps.validate.outcome }}\`
# #### Terraform Plan ?\`${{ steps.plan.outcome }}\`
# <details><summary>Show Plan</summary>
# \`\`\`\n
# ${process.env.PLAN}
# \`\`\`
# </details>
# *Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;
# github.rest.issues.createComment({
# issue_number: context.issue.number,
# owner: context.repo.owner,
# repo: context.repo.repo,
# body: output
# })
- name: Terraform Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
配置密钥
TF_API_TOKEN: 用于github action与terraform cloud进行通讯;
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY为 aws provider信息。
3.4 terraform cloud配置
申请tf 组织token用于存储
Team token:jwIwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8bM
配置terraform cloud apply method 为auto apply
四 测试
提交代码变更,查看
应用变化
4.2 查看目标云资源
VPC
4.3 查看web页面
由于aws 80端口需要备案,因此修改端口为8888
4.4 删除
五 注意事项
- 配置 Terraform Cloud API 密钥:在 GitHub 中,您需要在您的存储库设置中配置 Terraform Cloud API 密钥。此 API 密钥用于与 Terraform Cloud 进行通信并执行 Terraform 部署。确保保护此 API 密钥并遵循最佳安全实践。
- 遵循最佳实践:在使用 Terraform Cloud 和 GitHub Action 进行部署时,请遵循最佳实践和安全性建议,以确保您的基础结构得到保护和管理。
- 服务器80端口需要备案,使用8888端口,主机需要绑定密钥,通过用户ec2-user 登陆,绑定private网络,public网络关联网关,同一个vpc,不同子网中主角相互访问需要收费;
- GitHub action 集成terraform 目前测试使用TF_VAR传递敏感参数正常,其他异常。
总之,使用 Terraform Cloud 和 GitHub Action 进行自动化基础结构管理是一个强大的工具,可以帮助您节省时间并减少错误。但是,请确保您正确配置并遵循最佳实践和安全性建议,以确保您的基础结构得到保护和管理。
参考链接
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END