Terraform 是一种开源的基础设施即代码软件工具,使您能够安全、可预测地创建、更改和改进基础设施。 这是一篇比较长的博文,但 Terraform 正在变得越来越重要,它成为开发者技术栈中不可或缺的一部分,所以让我们从一些基础知识开始,然后慢慢过渡到 Terraform 和 Travis CI 更复杂的使用场景。 我们开始吧!
让我们看一下我的 terraform_config
文件
# Montana's Terraform config
provider "aws" {
region = "eu-west-1"
#access_key = "PUT-YOUR-ACCESS-KEY-HERE"
#secret_key = "PUT-YOUR-SECRET-KEY-HERE"
version = "~> 2.55.0"
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
required_version = ">= 0.13"
}
在配置文件中您会注意到我选择了 AWS
作为我的提供商,还有 ENVIRONMENT_VARIABLES
、Terraform 的版本以及我想要强制执行的版本。
我们假设这个项目处于共享环境中,因此 local state
不够,我们需要使用 remote state
。 简而言之,在 Terraform 中使用远程状态,Terraform 将状态数据写入远程数据存储,在本例中可能是云存储,然后可以与团队的所有成员共享。 可以把它想象成一个 Google 文档,您可以选择谁可以访问它。
在进入架构部分之前,请记住这个 Terraform 配置文件列表
credentials
credentials_helper
diable_checkpoint
disable_checkpoint_signature
plugin_cache_dir
provider_installation
因此,您作为一名开发者决定使用 Travis CI,它是互联网上最好的构建工具,首先,这是一个不错的选择! 以下是一些您需要了解的要点,以便顺利开始
我们知道 Travis 的作用,但是如果您不知道,在本例中,Travis 会构建网站工件,部署基础设施,并将工件推送到生产环境而不是暂存环境。
在本例中,我将使用 Amazon S3
后端以及 DynamoDB
来存储 Terraform 状态。 我发现 DynamoDB 与 Teraform 配合使用是最友好的。 Terraform 将状态(现在记住,不是本地状态,而是远程状态)存储在 S3 中,并使用 DynamoDB 在执行更改时获取锁。 锁定很重要,可以避免两个 Terraform 二进制文件同时修改同一状态。 如果两个 Terraform 实例正在执行此操作,那么您就会看到由此引发的麻烦和头痛。
让我们从 S3 后端配置文件开始
terraform {
required_version = ">= 0.12"
}
provider "aws" {}
data "aws_caller_identity" "current" {}
locals {
account_id = data.aws_caller_identity.current.account_id
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "${local.account_id}-terraform-states"
versioning {
enabled = true
}
# I strongly encourage enabling server side encryption by default - Montana
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
}
现在让我们创建 DynamoDB
Terraform 配置文件
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
您会看到已启用 "lock" 方法以强制执行仅允许一个 Terraform 实例生成。 接下来,您需要在这个过程中提供详细的信息,因此让我们设置一些 Terraform 输出
output "s3_bucket_name" {
value = aws_s3_bucket.terraform_state.id
description = "Montana's Terraform S3 Bucket"
}
output "s3_bucket_arn" {
value = aws_s3_bucket.terraform_state.arn
description = "Montana's ARN Bucket"
}
output "s3_bucket_region" {
value = aws_s3_bucket.terraform_state.region
description = "Montana's S3 Region of the Bucket"
}
output "dynamodb_table_name" {
value = aws_dynamodb_table.terraform_lock.name
description = "Montana's ARN of the DynamoDB table"
}
output "dynamodb_table_arn" {
value = aws_dynamodb_table.terraform_lock.arn
description = "The ARN of the DynamoDB table"
}
您可能会问自己,我们如何使用 Terraform 来设置我们想用于远程状态后端的 S3 存储桶和 DynamoDB 表? 我们有几种方法可以做到这一点
这两种解决方案都涉及使用 local state
创建 remote state
资源。 请记住,Terraform 状态可能包含机密信息。 如果仅是 Amazon S3
存储桶和 DynamoDB
表,则只有一个变量可能存在问题:AWS 访问密钥。
在 Terraform 中,如果您使用的是私有存储库,那么这可能不是什么大问题。 在处理开源代码时,在将状态文件提交到 GitHub 或您使用的任何 VCS 到存储库之前对其进行加密可能会很有用。 您可以使用 OpenSSL
甚至更专门的工具来执行此操作。
我们目前使用的是
让我们在本例中创建两个工作区 state
和 prod
。 state 工作区将管理远程状态资源,即 S3 存储桶和 DynamoDB 表。 prod
工作区将管理我们网站的生产环境。 稍后您可以添加更多工作区用于暂存或测试,但这超出了本文的范围。
让我们创建三个包含 Terraform 文件的文件夹,
现在,这就是您要关注的项目的结构
.
├── locals.tf
├── providers.tf
├── backend
│ ├── backend.tf
│ ├── backend.tf.tmpl
│ ├── locals.tf -> ../locals.tf
│ ├── providers.tf -> ../providers.tf
│ └── state.tf -> ../bootstrap/state.tf
├── bootstrap
│ ├── locals.tf -> ../locals.tf
│ ├── providers.tf -> ../providers.tf
│ └── state.tf
└── website
├── backend.tf -> ../backend/backend.tf
├── locals.tf -> ../locals.tf
├── providers.tf -> ../providers.tf
└── website.tf
让我们看一下 bootstrap/state.tf
的内容
# Montana's config files /bootstrap/state.tf
locals {
state_bucket_name = "${local.project_name}-${data.aws_caller_identity.current.account_id}-${data.aws_region.current.name}"
state_table_name = "${local.state_bucket_name}"
}
resource "aws_dynamodb_table" "locking" {
name = "${local.state_table_name}"
read_capacity = "20"
write_capacity = "20"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
resource "aws_s3_bucket" "state" {
bucket = "${local.state_bucket_name}"
region = "${data.aws_region.current.name}"
versioning {
enabled = true
}
server_side_encryption_configuration {
"rule" {
"apply_server_side_encryption_by_default" {
sse_algorithm = "AES256"
}
}
}
tags {
Name = "terraform-state-bucket"
Environment = "global"
project = "${local.project_name}"
}
}
output "BACKEND_BUCKET_NAME" {
value = "${aws_s3_bucket.state.bucket}"
}
output "BACKEND_TABLE_NAME" {
value = "${aws_dynamodb_table.locking.name}"
}
我在上面定义了 S3 存储桶,启用了加密和版本控制。 加密很重要,因为 Terraform 通常会包含 secrets
。
您会在 Terraform 中注意到我调用了一个名为 LockID
的属性,因此我们必须创建它并将其设为主键,以再次确保不会出现两个 Terraform 实例同时运行的情况。
让我们通过运行以下命令来创建 state
工作区
terraform workspace new state
terraform init bootstrap
terraform apply bootstrap
之后,将创建 S3 存储桶和 DynamoDB 表,我们将迁移本地状态。 让我们看一下 backend/backend.tf.tmpl
文件,这是 Terraform 将遵循的配置文件,您可以生成一个环境变量,或者在我的情况下,我从键值对设置了环境变量。 如果您不想采用传统方法,可以使用我提供的这个巧妙的 bash 脚本
#!/bin/bash
dotenv () {
set -a
[ -f .env ] && . .env
set +a
}
dotenv
cd () {
builtin cd $@
dotenv
}
现在,配置文件应该如下所示
terraform {
backend "s3" {
bucket = "${BACKEND_BUCKET_NAME}"
key = "terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "${BACKEND_TABLE_NAME}"
}
}
让我们初始化后端,运行
terraform init backend
现在您应该拥有一些核心资产,现在我们需要指定 HTML 和 CSS 文件。 这有点麻烦,因为我们无法像使用 wget
或 curl
一样告诉 Terraform 上传整个文件夹,但这正是 Terraform 的结构,让我们看一下 website.tf
locals {
site_root = "website/static/_site"
index_html = "${local.site_root}/index.html"
about_html = "${local.site_root}/about/index.html"
post_html = "${local.site_root}/jekyll/update/2018/06/30/welcome-to-jekyll.html"
error_html = "${local.site_root}/404.html"
main_css = "${local.site_root}/assets/main.css"
}
resource "aws_s3_bucket_object" "index" {
bucket = "${aws_s3_bucket.website.id}"
key = "index.html"
source = "${local.index_html}"
etag = "${md5(file(local.index_html))}"
content_type = "text/html"
}
resource "aws_s3_bucket_object" "post" {
bucket = "${aws_s3_bucket.website.id}"
key = "jekyll/update/2018/06/30/welcome-to-jekyll.html"
source = "${local.post_html}"
etag = "${md5(file(local.post_html))}"
content_type = "text/html"
}
resource "aws_s3_bucket_object" "about" {
bucket = "${aws_s3_bucket.website.id}"
key = "about/index.html"
source = "${local.about_html}"
etag = "${md5(file(local.about_html))}"
content_type = "text/html"
}
resource "aws_s3_bucket_object" "error" {
bucket = "${aws_s3_bucket.website.id}"
key = "error.html"
source = "${local.error_html}"
etag = "${md5(file(local.error_html))}"
content_type = "text/html"
}
resource "aws_s3_bucket_object" "css" {
bucket = "${aws_s3_bucket.website.id}"
key = "assets/main.css"
source = "${local.main_css}"
etag = "${md5(file(local.main_css))}"
content_type = "text/css"
}
output "url" {
value = "http://${local.website_bucket_name}.s3-website.${aws_s3_bucket.website.region}.amazonaws.com"
}
现在让我们运行一些最终命令
terraform workspace new prod
terraform init website
cd website/static && jekyll build && cd -
terraform apply website
我们现在已经使用 Terraform 和 DynamoDB 创建了一个静态网站。 现在让我们实现 Travis!
让我们开始实现 Travis 来捕获任何错误! 您知道我们需要创建 .travis.yml.
,简而言之,它会告诉构建服务器要执行哪些命令。 让我们看一下 .travis.yml
文件
---
language: generic # this can also be left out
install:
- gem install bundler jekyll
script:
- ./build.sh
我们需要那个 build.sh
文件,但请记住要为它赋予适当的权限,以下是其内容
cd website/static
bundle install
bundle exec jekyll build
cd -
./terraform-linux init
./terraform-linux validate website
if [[ $TRAVIS_BRANCH == 'master' ]]
then
./terraform-linux workspace select prod
./terraform-linux apply -auto-approve website
fi
如果可能的话,我可能会选择仅使用本地状态并将它直接存储在存储库中,这将使许多步骤变得更轻松,幸运的是,我专门为 Terraform 和 Travis 创建了一个 bash 脚本,以简化操作。 这是针对测试环境的,因此请确保在使用它之前,您是在暂存环境或某种测试环境中运行它。
#!/bin/bash
set=+x
/ # set -v && echo $HOME
/root
/ # set +v && echo $HOME
set +v && echo $HOME
/root
/ # set -x && echo $HOME
+ echo /root
/root
/ # set +x && echo $HOME
+ set +x
/root
# Check the creds made sure by Montana Mendy.
cred="-var "do_token=${DO_PAT}""
tf=$(which terraform)
init() {
echo ""
echo "Init the provider"
echo ""
$tf init
echo "Formatting files"
echo ""
$tf fmt
echo ""
echo "Validating files"
echo ""
$tf validate
}
clone() {
if [ -d ansible ]; then
rm -rf ansible
git clone --depth=1 $ansible_repo
echo ""
else
git clone --depth=1 $ansible_repo
echo ""
fi
if [ -d shell_scripts ]; then
rm -rf shell_scripts
git clone --depth=1 $shell_script_repo
echo ""
else
git clone --depth=1 $shell_script_repo
echo ""
fi
if [ -d cloud_init ]; then
rm -rf cloud_init
git clone --depth=1 $cloud_init_repo
echo ""
else
git clone --depth=1 $cloud_init_repo
echo ""
fi
}
# Clean up shell scripts.
clean() {
if [ -d shell_scripts ]; then
rm -rf shell_scripts
fi
if [ -d ansible ]; then
rm -rf ansible
if [ -f inventory ]; then
rm inventory
fi
fi
if [ -d cloud_init ]; then
rm -rf cloud_init
fi
if [ -f terraform.tfstate ]; then
rm -rf terraform*
rm -rf .terraform
fi
}
# Run Terraform commands.
plan() {
clone
init
$tf plan $cred
}
apply() {
clone
init
$tf apply -auto-approve $cred
}
case $1 in
-i | init)
$tf init
;;
clone)
clone
;;
plan)
plan
;;
apply)
apply
;;
show)
$tf show
;;
list)
$tf state list
;;
# Run 'terraform destroy'.
destroy)
$tf destroy $cred
clean
;;
clean)
clean
;;
-f | fmt)
$tf fmt
;;
-v | validate)
$tf validate
;;
*)
echo "$0 [options]"
echo ""
echo "Options are: clone | plan | apply| show| list | destroy | fmt | validata"
echo ""
;;
esac
好了,我们使用 Terraform、Travis CI、Amazon、API Gateway 创建并部署了一个 Jekyll 网站,还附带了我的一些提示和技巧,我认为您在使用它进行配置时可能会发现这些提示和技巧很有用。
如果您有任何问题,请发送电子邮件至 [email protected].
一如既往,祝您构建愉快!