自分専用クラウドストレージをTerraformで自動構築
背景
世の中にGoogle DriveやDropboxといった便利なサービスがありますが、プライバシーの観点から自分が管理できるクラウドストレージが欲しくなってきました
S3とCloudFrontを利用すればセキュアな自分用クラウドストレージが作成できるのではと思いつきやってみました。
構築
ディレクトリ構成
思いつきで変更して後で再現できないことになったら泣きそうになるので、構築にはAWSとTerraformを利用します。
色々な方がディレクトリ構成案を考えてくれていますが、今回はシンプルにいきます。
$ tree ./
.
├── aws.tfvars
├── Makefile
├── contents
│ └── index.html # 動作確認用のHTMLファイル
├── main.tf
└── modules
├── cloudfront.tf
├── main.tf
└── s3-storage.tf
└── s3-logs.tf
秘密情報
IAMユーザのアクセスキー、シークレットキーなどの機密情報はaws.tfvars
に保存します。
aws.tfvars
はコミットゼッタイダメ
(予定はないですが)海外移住に備えてリージョン情報もついでに記載します。
aws_access_key = "AKIAZUH76..."
aws_secret_key = "OhNKSFXXl9bn..."
aws_region = "ap-northeast-1"
機密情報の読み込みはvariable
セクションにで行います。
aws.tfvars
の左辺の文字列とvariable
の名前を一致させると読み込ませることができます。
variable "aws_access_key" {
type = string
sensitive = true // コンソールに値を直接出さない
description = "The access key for your IAM user in AWS."
}
variable "aws_secret_key " {
type = string
sensitive = true
description = "The secret access key for your IAM user in AWS."
}
variable "aws_region" {
type = string
description = "The region name to deployment."
}
Provider
aws.tfvars
で設定した情報を読み込み。terraform.required_providers.aws.version
は下記ページのパンくずリストから利用したいバージョンを確認し設定してください。
Terraform Registry
今回作成したリソースをコンソール上でも区別できるように全てのリソースにタグを付けます。
全てのリソースに共有したタグを付けるにはdefault_tags
を利用します。今回はPersonal cloud storage
というName
タグを付けています。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 4.61.0"
}
}
}
provider "aws" {
region = var.aws_region
access_key = var.aws_access_key
secret_key = var.aws_secret_key
default_tags {
tags = {
"Name" = "Personal cloud storage"
}
}
}
module "aws" {
source = "./modules"
}
// CloudFrontのドメイン名を表示
output "cloudfront_distribution_domain_name" {
value = module.aws.cloudfront_distribution_domain_name
}
S3
S3は配信するファイル配置するバケットとログを保存するバケットの2つ用意します。
配信用バケットには動作確認用の最低限のことがかかれたHTMLファイルをデプロイ時に配置します。
resource "aws_s3_bucket" "access_logs" {
bucket = "personal-cloud-storage-57356-logs"
force_destroy = true
}
// パブリックアクセス無効
resource "aws_s3_bucket_public_access_block" "access_logs" {
bucket = aws_s3_bucket.access_logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
// サーバサイドで暗号化
resource "aws_s3_bucket_server_side_encryption_configuration" "server_access_logs" {
bucket = aws_s3_bucket.access_logs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256" // SSE-S3
}
}
}
// ACL設定
data "aws_canonical_user_id" "current" {}
resource "aws_s3_bucket_acl" "access_logs" {
bucket = aws_s3_bucket.access_logs.id
access_control_policy {
grant {
grantee {
id = data.aws_canonical_user_id.current.id
type = "CanonicalUser"
}
permission = "FULL_CONTROL"
}
grant {
grantee {
type = "Group"
uri = "http://acs.amazonaws.com/groups/s3/LogDelivery"
}
permission = "FULL_CONTROL"
}
owner {
id = data.aws_canonical_user_id.current.id
}
}
}
// ライフサイクル設定 : 1年保存設定
resource "aws_s3_bucket_lifecycle_configuration" "access_logs" {
bucket = aws_s3_bucket.access_logs.id
rule {
id = "expiration-rule"
status = "Enabled"
expiration {
days = 365
}
}
}
// バケットポリシー
data "aws_caller_identity" "current" {}
data "aws_iam_policy_document" "access_logs" {
// アクセスログのPutを許可
statement {
sid = "S3ServerAccessLogsPolicy"
effect = "Allow"
principals {
identifiers = ["*"]
type = "*"
}
actions = [
"s3:ListBucket",
"s3:PutObject",
"s3:GetObject"
]
resources = [
"${aws_s3_bucket.access_logs.arn}",
"${aws_s3_bucket.access_logs.arn}/*"
]
condition {
test = "ArnLike"
variable = "aws:SourceArn"
values = [
"arn:aws:s3:::*"
]
}
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values = [
"${data.aws_caller_identity.current.account_id}" // 自アカウントからのみ操作を許可
]
}
}
}
resource "aws_s3_bucket_policy" "access_logs" {
bucket = aws_s3_bucket.access_logs.bucket
policy = data.aws_iam_policy_document.access_logs.json
}
resource "aws_s3_bucket" "pcs" {
bucket = "personal-cloud-storage-57356" // 世界でユニークな名前 末尾に好きな数字を5桁くらい入れれば大丈夫
force_destroy = true // オブジェクトがあっても強制削除
}
// 動作確認用HTMLファイルをバケットに配置
resource "aws_s3_object" "pcs" {
bucket = aws_s3_bucket.pcs.id
key = "index.html"
source = "contents/index.html"
content_type = "text/html"
}
// パブリックアクセス無効
resource "aws_s3_bucket_public_access_block" "pcs" {
bucket = aws_s3_bucket.pcs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
// サーバサイドで暗号化
resource "aws_s3_bucket_server_side_encryption_configuration" "pcs" {
bucket = aws_s3_bucket.pcs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256" // SSE-S3
}
}
}
// ACL無効 (バケット所有者の強制)
resource "aws_s3_bucket_ownership_controls" "pcs" {
bucket = aws_s3_bucket.pcs.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
// アクセスログ送信設定
resource "aws_s3_bucket_logging" "pcs" {
bucket = aws_s3_bucket.pcs.id
target_bucket = aws_s3_bucket.access_logs.id // ログ送信先バケット
target_prefix = "s3" // ログ送信先Prefix
}
// 誤削除対策にバージョニングを有効化
resource "aws_s3_bucket_versioning" "pcs" {
bucket = aws_s3_bucket.pcs.id
versioning_configuration {
status = "Enabled"
}
}
// CloudFrontからのみアクセスできるバケットポリシー
data "aws_iam_policy_document" "pcs" {
version = "2012-10-17"
statement {
sid = "AllowCloudFrontServicePrincipal"
effect = "Allow"
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
actions = [
"s3:GetObject"
]
resources = [
"${data.aws_s3_bucket.pcs.arn}/*"
]
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.pcs.arn]
}
}
}
// バケットポリシーを設定
resource "aws_s3_bucket_policy" "pcs" {
bucket = data.aws_s3_bucket.pcs.id
policy = data.aws_iam_policy_document.pcs.json
}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Personal Cloud Storage</title>
</head>
<body>
<h1>Personal Cloud Storage</h1>
<div>Test page.</div>
</body>
</html>
今回はバケットポリシーの設定方法がわからずだいぶ時間をかけてしまいました。
data
で設定後、jsonに変換して設定する方法を初めて知りました。公式ドキュメントの確認は大事。
CloudFront
S3からの直接配信ではHTTPのみなのでHTTPS化するためにCloudFrontをS3の前に配置します。
resource "aws_cloudfront_origin_access_control" "pcs" {
name = "cf-oac-with-tf-pcs"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_distribution" "pcs" {
enabled = true
comment = "自分専用クラウドストレージ"
default_root_object = "index.html"
price_class = "PriceClass_200" // コスト面を考えて配信地域を北米,ヨーロッパ,アジアに限定
origin {
domain_name = "personal-cloud-storage-57356.s3.ap-northeast-1.amazonaws.com"
origin_id = "personal-cloud-storage-57356"
origin_access_control_id = aws_cloudfront_origin_access_control.pcs.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"] // 今回はダウンロードのみなのでGET, HEADのみ
cached_methods = ["GET", "HEAD"]
target_origin_id = "personal-cloud-storage-57356"
compress = true
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = ["JP"]
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
// アクセスログの保管場所設定
logging_config {
bucket = "personal-cloud-storage-57356-logs.s3.amazonaws.com"
include_cookies = true
prefix = "cloudfront/"
}
}
Output
CloudFrontへのドメイン名を構築後にコンソールへ表示させます。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.61.0"
}
}
}
output "cloudfront_distribution_domain_name" {
value = aws_cloudfront_distribution.pcs.domain_name
}
実行
実行時にオプションでaws.tfvars
を指定して実行。
指定しない場合、インタラクティブに入力します。
$ terraform plan -var-file aws.tfvars
私は時間が経つと忘れるので、Makefileにコマンドをまとめています
$ cat Makefile
---
plan:
terraform plan -var-file aws.tfvars
apply:
terraform apply -var-file aws.tfvars
format:
terraform fmt -recursive
destroy:
terraform destroy -var-file aws.tfvars
---
$ make plan # 実行計画を表示
$ make apply # デプロイ
※注意点※
S3バケットの削除
この環境はCloudFrontとS3バケットを一緒に作ってしまっているのでdestroyをすると一緒に消えてしまいます。通常はオブジェクトが入った状態では削除時にエラーがでますが、下記のように設定しているためオブジェクトごと削除されます。
resource "aws_s3_bucket" "pcs" {
bucket = "personal-cloud-storage-57356" // 世界でユニークな名前 末尾に好きな数字を5桁くらい入れれば大丈夫
force_destroy = true // オブジェクトがあっても強制削除
}
例えば、CloudFrontに独自ドメインとACMの証明書を設定するために一旦削除とするといままで登録していたファイルごといかれます。私の調査方法が悪かったのか、片方のリソースだけを消すうまい方法を見つけることができませんでした。なので、私はCloudFrontとS3を別々のフォルダに分け、それぞれapplyしています。
この方法だとCloudFront側でS3の補完が効かないのでdataでバケットを定義しています。
うまい方法をご存じであればコメント欄にてご教示ください。
認証
今回の構成ではCloudFront distributionのURLを知られると誰でもアクセス可能になってしまいます。リクエストヘッダに秘密のトークンを付け、CloudFront側はLambda@Edgeで検証するといった認証機能が必要です。
まとめ
AWSのCloudFrontとS3を利用したファイル配信環境をTerraformを利用して構築してみました。サービスは2つしか使っていないのに思った以上のコード量になってしまいました。やはり自動化は難しいですね。
今はアップロードはAWSコンソールからなのでブラウザからアップロードする方法とファイル一覧表示する機能を考えたいです。
料金がいくらかかるのか気になって試算しました。DropBoxで2TBを使おうとすると1,500円/月。
S3標準で2TBを使おうとすると東京リージョンでは0.025USD/GB*2000GB=50USD …
特別な理由がない限りはSaaSを使いましょう
拙文最後までお読みいただきありがとうございます。
気軽にいいね!、Twitterフォローをお願いします。
Discussion