通过Terraform,将静态文件传递服务器的基础设施调整为可进行复制粘贴的状态
在中国南方,有一座美丽的小城市。这座城市坐落在蓝天和翠绿山脉的映衬下,风景如画。人们在这里过着宁静而宜人的生活,远离城市的喧嚣和压力。
-
- 個人開発アプリを作る時に、LP等の用途で静的ファイルを単純に保存・配信するだけのサーバーが必要になることが多い。
-
- 毎回 AWS Management Console から同じ手順で作っていたが、環境ごとに設定したりするので地味に面倒くさい
-
- terraform で「コードをコピペするだけで S3, CloudFront, ACM, Route53 周りの設定を完了できる」ような状態にしたい
相手の本棚を覗けるマッチングサービス「MatchLab」の LP を terraform で管理できるようにしてみた。
索引
-
- terraform とは
-
- 使い方
-
- 既存のインフラを terraform 管理に移行する
- GitLab CI/CD で変更を検知して自動化
terraform 是什么?
-
- https://www.terraform.io
-
- AWS 等のインフラの設定を宣言的に書いておくことで、コードに応じてリソースを新規作成したり設定変更したり良い感じに管理してくれるIaCツール
- コードを書いて CLI 上で terraform plan terraform apply などと唱えると現在のインフラの状態と設定の変更点を確認して適用してくれる
用法
前提- 一开始的条件或假设
-
- 作業用の IAM ユーザーを発行して、 Route53, S3, CloudFront, ACM の権限を与えておく
-
- 作業スペースに AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY をそれぞれ指定しておく
-
- terraform の設定ファイルを置く用の S3 を作っておく
-
- CDN のホスト名にするために Route53 に Hosted Zone を作っておく
- ACM で上記のホスト名および配下のサブドメインの暗号化に対応できる SSL Certificate を発行しておく
在本地安裝 Terraform。
如果您使用的是 Mac,可以通过 homebrew 来安装。其他情况请参考 https://www.terraform.io/downloads。
$ brew tap hashicorp/tap
$ brew install hashicorp/tap/terraform
由于我觉得在更换电脑时进行重新设置很麻烦,所以将其Docker化,并在容器内进行操作。
FROM hashicorp/terraform:1.1.7
WORKDIR /app
对于刚开始使用的人来说,可以在DockerHub上检查并安装hashicorp/terraform的最新版本,这样会很不错。
version: "3"
services:
terraform:
build: ./terraform
volumes:
- ./terraform:/app
env_file:
- ./env/terraform.env
需要注意的是,对于 Docker,hashicorp/terraform镜像已指定entrypoint,因此进入容器时需要覆盖entrypoint。
$ docker-compose run --rm --entrypoint sh terraform
只需要在最后设置 terraform.env 文件中的 AWS_ACCESS_KEY_ID 和 AWS_SECRET_ACCESS_KEY,开发环境就会完成。
最初的设定
首先,编写设置以将当前基础设施状态记录在S3上的tfstate文件中。
terraform {
backend "s3" {
bucket = "terraform.match-lab.com"
key = "staging/terraform.tfstate"
region = "ap-northeast-1"
}
}
接下来指定terraform要连接的平台。
provider "aws" {
region = "ap-northeast-1"
}
provider "aws" {
region = "us-east-1"
alias = "us-east-1"
}
在AWS的情况下,需要针对每个地区指定提供者。
通常只需指定ap-northeast-1即可,但由于本次将ACM资源创建在us-east-1,因此需要添加两个以便能够访问us-east-1。
接下来,需要指定 terraform 的版本以及用于了解 AWS 服务和配置项的 provider 版本。
terraform {
required_version = "1.1.7"
required_providers {
aws = "4.4.0"
}
}
如果提供者的版本过旧,则无法访问AWS的最新设置项,因此对于新手来说,最好查看Terraform Registry并指定最新版本。
如果所有以上的文件都能够放置在同一个目录中,那么在该目录下执行以下命令。
$ terraform init
这样做的话会生成一个名为.terraform.lock.hcl的文件和一个名为.terraform的文件夹,然后以好的方式结束。如果使用git管理,最好忽略.terraform文件夹。
至此,初始设置已经完成。
写设置文件
为每个环境设置不同的变量值。
为了在每个环境中复制粘贴代码时尽量不考虑任何事情,我希望能够将每个环境中的不同值集中管理到一个文件中。因此,我创建了以下文件。
locals {
domain_name = "match-lab.com"
bucket_name = "lp.match-lab.com"
static_host_name = "match-lab.com"
}
-
- domain_name: Route53 で取得した Hosted Zone 名
-
- bucket_name: 静的ファイルを保存する S3 のバケット名
- static_host_name: CloudFront から配信する際のホスト名(サブドメイン可)
在这里定义的变量可以从每个设置文件中进行引用。
开展必要的服务
# S3 bucket
resource "aws_s3_bucket" "static" {
bucket = local.bucket_name
force_destroy = false
}
# CloudFront が S3 にファイルを取りに行く時の identity
resource "aws_cloudfront_origin_access_identity" "static" {
comment = "access-identity-${aws_s3_bucket.static.bucket_regional_domain_name}"
}
# S3 bucket policy で上記の identity に GetObject の権限を付与する
resource "aws_s3_bucket_policy" "static" {
bucket = aws_s3_bucket.static.id
policy = jsonencode(
{
Id = "PolicyForCloudFrontPrivateContent"
Statement = [
{
Action = "s3:GetObject"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${aws_cloudfront_origin_access_identity.static.id}"
}
Resource = "arn:aws:s3:::${local.bucket_name}/*"
Sid = "1"
}
]
Version = "2008-10-17"
}
)
}
# bucket 上の object へのアクセス権限を管理するためのリソース
# ref: https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html
resource "aws_s3_bucket_ownership_controls" "static" {
bucket = aws_s3_bucket.static.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
# CloudFront distribution
resource "aws_cloudfront_distribution" "static_cdn" {
enabled = true
origin {
domain_name = aws_s3_bucket.static.bucket_regional_domain_name
origin_id = aws_s3_bucket.static.bucket_regional_domain_name
s3_origin_config {
origin_access_identity = "origin-access-identity/cloudfront/${aws_cloudfront_origin_access_identity.static.id}"
}
}
aliases = [local.static_host_name]
default_root_object = "index.html"
is_ipv6_enabled = true
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = aws_s3_bucket.static.bucket_regional_domain_name
viewer_protocol_policy = "redirect-to-https"
cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # CloudFront にデフォルトで用意されている Cache Policy の ID
compress = true
}
viewer_certificate {
acm_certificate_arn = data.aws_acm_certificate.certificate.id
cloudfront_default_certificate = false
minimum_protocol_version = "TLSv1.2_2021"
ssl_support_method = "sni-only"
}
restrictions {
geo_restriction {
restriction_type = "none"
locations = []
}
}
}
# CDN 配信時に利用する Route53 Record
# CloudFront distribution への alias になっている
resource "aws_route53_record" "static" {
zone_id = data.aws_route53_zone.primary.zone_id
name = local.static_host_name
type = "A"
alias {
evaluate_target_health = false
name = aws_cloudfront_distribution.static_cdn.domain_name
zone_id = aws_cloudfront_distribution.static_cdn.hosted_zone_id
}
}
从多个环境中读取用作外部资源的资源。
我正在考虑针对每个环境建立一个静态文件分发服务器的用例。
假设在这种情况下,生产环境为 https://match-lab.com,开发环境为 https://dev.match-lab.com,AWS Route53的托管区和ACM的证书将在多个环境中共享一个。
如果要直接使用terraform来管理这种情况,那么当在其中一个环境中应用更改时,共享的资源将同时更改,导致在另一个环境中也会发生更改。
因此,考虑到资源的变更频率较低,我们决定在 terraform 管理之外手动创建并将其作为外部资源加载到 terraform 中。
data "aws_route53_zone" "primary" {
name = local.domain_name
}
data "aws_acm_certificate" "certificate" {
provider = aws.us-east-1
domain = local.domain_name
}
通过这样,您可以以 data.aws_route53_zone.primary 和 data.aws_acm_certificate.certificate 的名称访问现有资源。
确认设定变更点
由于已经齐备了所需的文件,现在将应用更改并进行模拟以确认结果。
$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
+ create
Terraform will perform the following actions:
# aws_cloudfront_distribution.static_cdn will be created
+ resource "aws_cloudfront_distribution" "static_cdn" {
+ aliases = [
+ "match-lab.com",
]
+ arn = (known after apply)
+ caller_reference = (known after apply)
+ default_root_object = "index.html"
+ domain_name = (known after apply)
+ enabled = true
+ etag = (known after apply)
+ hosted_zone_id = (known after apply)
+ http_version = "http2"
+ id = (known after apply)
+ in_progress_validation_batches = (known after apply)
+ is_ipv6_enabled = true
+ last_modified_time = (known after apply)
+ price_class = "PriceClass_All"
+ retain_on_delete = false
+ status = (known after apply)
+ tags_all = (known after apply)
+ trusted_key_groups = (known after apply)
+ trusted_signers = (known after apply)
+ wait_for_deployment = true
+ default_cache_behavior {
+ allowed_methods = [
+ "GET",
+ "HEAD",
]
+ cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
+ cached_methods = [
+ "GET",
+ "HEAD",
]
+ compress = true
+ default_ttl = (known after apply)
+ max_ttl = (known after apply)
+ min_ttl = 0
+ target_origin_id = (known after apply)
+ trusted_key_groups = (known after apply)
+ trusted_signers = (known after apply)
+ viewer_protocol_policy = "redirect-to-https"
}
+ origin {
+ connection_attempts = 3
+ connection_timeout = 10
+ domain_name = (known after apply)
+ origin_id = (known after apply)
+ s3_origin_config {
+ origin_access_identity = (known after apply)
}
}
+ restrictions {
+ geo_restriction {
+ locations = (known after apply)
+ restriction_type = "none"
}
}
+ viewer_certificate {
+ acm_certificate_arn = "arn:aws:acm:us-east-1:270422322105:certificate/bfa4a6f0-3487-460b-84fb-fb845b9440ef"
+ cloudfront_default_certificate = false
+ minimum_protocol_version = "TLSv1.2_2021"
+ ssl_support_method = "sni-only"
}
}
# aws_cloudfront_origin_access_identity.static will be created
+ resource "aws_cloudfront_origin_access_identity" "static" {
+ caller_reference = (known after apply)
+ cloudfront_access_identity_path = (known after apply)
+ comment = (known after apply)
+ etag = (known after apply)
+ iam_arn = (known after apply)
+ id = (known after apply)
+ s3_canonical_user_id = (known after apply)
}
# aws_route53_record.static will be created
+ resource "aws_route53_record" "static" {
+ allow_overwrite = (known after apply)
+ fqdn = (known after apply)
+ id = (known after apply)
+ name = "match-lab.com"
+ type = "A"
+ zone_id = "Z28PJVS3TJX39E"
+ alias {
+ evaluate_target_health = false
+ name = (known after apply)
+ zone_id = (known after apply)
}
}
# aws_s3_bucket.static will be created
+ resource "aws_s3_bucket" "static" {
+ acceleration_status = (known after apply)
+ acl = (known after apply)
+ arn = (known after apply)
+ bucket = "lp.match-lab.com"
+ bucket_domain_name = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ cors_rule = (known after apply)
+ force_destroy = false
+ grant = (known after apply)
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ lifecycle_rule = (known after apply)
+ logging = (known after apply)
+ object_lock_enabled = (known after apply)
+ policy = (known after apply)
+ region = (known after apply)
+ replication_configuration = (known after apply)
+ request_payer = (known after apply)
+ server_side_encryption_configuration = (known after apply)
+ tags_all = (known after apply)
+ versioning = (known after apply)
+ website = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
+ object_lock_configuration {
+ object_lock_enabled = (known after apply)
+ rule = (known after apply)
}
}
# aws_s3_bucket_ownership_controls.static will be created
+ resource "aws_s3_bucket_ownership_controls" "static" {
+ bucket = (known after apply)
+ id = (known after apply)
+ rule {
+ object_ownership = "BucketOwnerEnforced"
}
}
# aws_s3_bucket_policy.static will be created
+ resource "aws_s3_bucket_policy" "static" {
+ bucket = (known after apply)
+ id = (known after apply)
+ policy = (known after apply)
}
Plan: 6 to add, 0 to change, 0 to destroy.
───────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you
run "terraform apply" now.
可以事先知道哪些资源会被添加/更改/删除,所以在应用之前可以经常确认,这样就可以放心了。
应用设置更改
$ terraform apply
当被确认可以执行时,回答”是”后将会执行操作。
它会立即帮我们创建新的S3 bucket、CloudFront Distribution和Route53 Record等。
为了确认操作是否成功,尝试将一个图片文件上传到新建的S3,并打开指定的域名,图片顺利显示出来。
处理不再需要的环境
当需要删除整个环境时,只需一个命令即可完成删除。
(由于文件存在于S3中,无法直接删除存储桶,因此需要事先删除文件。)
在执行删除命令之前,先确认实际将删除什么内容。
$ terraform plan -destroy
执行删除命令前请确认没有问题。
$ terraform destroy
用这个方法可以完全删除。
将现有的基础设施迁移到 Terraform 管理下
有时候,已经通过 AWS 管理控制台创建的资源希望后来转移到 terraform 进行管理。
在这种情况下,可以通过将现有资源反映到 tfstate 文件中,来实现迁移 → 然后修改配置文件,以确保 tfstate 文件与资源一致,最后完成迁移的步骤。
将现有资源同步到 tfstate 中
使用 Terraform import 命令。
例如,要将现有的 S3 存储桶反映到 tfstate 中,可以先在配置文件中添加 resource 块,然后执行命令。
resource "aws_s3_bucket" "static" {
}
$ terraform import aws_s3_bucket.static match-lab.com
当您想要查找要导入的资源名称时,可以在文档中进行搜索,立即就能找到。
顺便提一下,如果不小心导入了错误的资源,直接删除资源块后,下一次应用时导入的资源将被删除,因此需要将其从tfstate管理中排除。
$ terraform state rm aws_s3_bucket.static
为了确保与tfstate没有差异,修改配置文件。
使用terraform plan命令可以显示缺少的配置项以及在当前情况下执行会产生的差异,因此需要填补配置文件中的差异部分。
$ terraform plan
一旦没有差异,即可应用并且不会产生任何变化,这样我们就可以顺利地将其纳入terraform的管理之下。太棒了!
通过GitLab CI/CD检测到变更并自动化操作。
我的一个IaC的重要优点是能够通过版本管理来管理变更,因此我希望在合并PR时能够自动在CI中应用。
由于这个项目使用的是GitLab,所以我使用了GitLab CI/CD并进行了以下配置。
cache:
paths:
- ./terraform/.terraform
terraform-apply-staging:
image:
name: hashicorp/terraform:1.1.7
entrypoint: [""]
stage: deploy
environment:
name: terraform
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
changes:
- terraform/**/*
script:
- cd ${CI_PROJECT_DIR}/terraform
- terraform init
- terraform apply -auto-approve
-
- 開発環境と同様に Docker の hashicorp/terraform イメージを利用した
-
- entrypoint を [“”] で上書きすることで script を実行できるようにした
-
- master ブランチにマージされた時点で apply を実行するようにした
-
- CI 上で yes と回答することができずエラーになるので -auto-approve オプションを指定した
.terraform ディレクトリはバージョンを変更しない限り毎回同じなので cache した
总结
在导入现有资源的过程中,我意识到在不同环境中存在不同的配置差异,并能够统一这些差异,同时通过查阅 Terraform 文档,我发现了一些我不熟悉的配置项,这让我学到了很多东西。
GitLab 官方有一个类似于为 Terraform 创建的配置集合的 GitLab CI,当我阅读它时,可以了解到一些新的设置项,非常方便。
我觉得以后要搭建静态文件传输服务器的话,只需复制粘贴这个,就能轻松完成,感觉会轻松很多。