Changes
12 changed files (+331/-3)
-
-
@@ -28,3 +28,8 @@ # YAML は 2 スペース以外のインデントを仕様レベルで読むことができない。[*.{yml,yaml}] indent_style = space indent_size = 2 # terraform fmt は 2 スペース以外の指定ができない。 [*.tf] indent_style = space indent_size = 2
-
-
-
@@ -4,3 +4,31 @@# What: Node.js 及び互換ツールが依存パッケージを格納するディレクトリ。 # Why: サイズが非常に大きく、可読性も低いため。 node_modules # What: Terraform のプラットフォームファイルやらが詰まったディレクトリ。 # Why: プラットフォーム毎に異なる内容が含まれるため。 .terraform # What: Terraform のパラメータファイル。 # Why: デプロイ先環境によって異なったり、シークレットが含まれたりするため。 *.tfvars *.tfvars.json # What: Terraform の状態ファイル。 # Why: センシティブな情報盛りだくさん。 *.tfstate *.tfstate.* # What: Terraform の設定上書きファイル。 # Why: コードをチェックインせずにワーク上だけで上書きするものであるため。 override.tf override.tf.json *_override.tf *_override.tf.json # What: `terraform apply` で一時的に生成されるファイル。 .terraform.tfstate.lock.info # What: 環境変数。 # Why: シークレットや環境依存の値が含まれるため。 .env
-
-
-
@@ -3,5 +3,6 @@ # バージョン管理ツールでロードするソフトウェアとバージョンの一覧。# # SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only dprint 0.47.5 bun 1.1.38 dprint 0.47.5 bun 1.1.38 terraform 1.10.3
-
-
-
-
@@ -15,6 +15,10 @@ "commands": [{ "exts": ["proto"], "command": "bunx buf format {{file_path}}" }, { "exts": ["tf"], "command": "terraform fmt -" } ] },
-
-
mise.toml (new)
-
@@ -0,0 +1,8 @@# SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only [env] _.file = ".env" [tools] awscli = "2.22.5"
-
-
-
@@ -0,0 +1,25 @@# This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { version = "4.67.0" constraints = "~> 4.16" hashes = [ "h1:5Zfo3GfRSWBaXs4TGQNOflr1XaYj6pRnVJLX5VAjFX4=", "zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060", "zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6", "zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183", "zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1", "zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29", "zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7", "zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043", "zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362", "zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", "zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf", "zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b", "zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c", "zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c", "zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d", ] }
-
-
-
@@ -0,0 +1,2 @@SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> SPDX-License-Identifier: AGPL-3.0-only
-
-
-
@@ -0,0 +1,91 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import * as zod from "zod"; const DIST_DIR = new URL("../../dist", import.meta.url); const outputFileSchema = zod.object({ cloudfront_distribution_id: zod.object({ type: zod.literal("string"), value: zod.string(), }), static_file_bucket_name: zod.object({ type: zod.literal("string"), value: zod.string(), }), }); interface TerraformOutput { bucketName: string; cfDistributionID: string; } async function getTerraformOutput(): Promise<TerraformOutput> { const output = Bun.spawnSync(["terraform", "output", "-json"], { stdin: "inherit", }); const stdout = JSON.parse(await new Response(output.stdout).text()); const result = outputFileSchema.safeParse(stdout); if (!result.success) { throw result.error; } return { bucketName: result.data.static_file_bucket_name.value, cfDistributionID: result.data.cloudfront_distribution_id.value, }; } async function deploy(opts: TerraformOutput): Promise<void> { const s3Sync = Bun.spawnSync( ["aws", "s3", "sync", "--delete", DIST_DIR.pathname, `s3://${opts.bucketName}`], { stdin: "inherit", stdout: "inherit", stderr: "inherit", }, ); if (!s3Sync.success) { throw new Error("aws s3 sync コマンドが異常終了しました"); } const cfInvalidation = Bun.spawnSync( [ "aws", "cloudfront", "create-invalidation", "--distribution-id", opts.cfDistributionID, "--paths", "/*", ], { stdin: "inherit", stdout: "inherit", stderr: "inherit", }, ); if (!cfInvalidation.success) { throw new Error("aws cloudfront create-invalidation コマンドが異常終了しました"); } } async function main() { const output = await getTerraformOutput(); await deploy(output); } if (import.meta.main) { try { main(); } catch (error) { console.error(error); process.exit(1); } }
-
-
-
@@ -0,0 +1,150 @@# S3+Cloudfront 構成のサイトインフラ。独自ドメインを CNAME で Cloudfront の # ドメインに向ける前提。 ACM のバリデーションは output をコピペで手動。 # # SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.16" } } required_version = ">= 1.2.0" } provider "aws" { region = var.aws_region default_tags { tags = { Service = "Yamori" Module = "PWA" } } } provider "aws" { alias = "us_east_1" region = "us-east-1" default_tags { tags = { Service = "Yamori" Module = "PWA" } } } resource "aws_s3_bucket" "origin" {} output "static_file_bucket_name" { value = aws_s3_bucket.origin.id } data "aws_iam_policy_document" "s3_cf_read_policy" { statement { sid = "AllowCloudfrontReadonly" principals { type = "Service" identifiers = ["cloudfront.amazonaws.com"] } actions = ["s3:GetObject"] resources = ["${aws_s3_bucket.origin.arn}/*"] condition { test = "StringEquals" variable = "aws:SourceArn" values = [aws_cloudfront_distribution.cdn.arn] } } } resource "aws_s3_bucket_policy" "allow_read_from_cloudfront" { bucket = aws_s3_bucket.origin.id policy = data.aws_iam_policy_document.s3_cf_read_policy.json } resource "aws_acm_certificate" "domain_cert" { # CloudFront で使う ACM は us-east-1 にある必要がある。 # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html provider = aws.us_east_1 domain_name = var.pwa_domain validation_method = "DNS" lifecycle { create_before_destroy = true } } output "domain_cert_validations" { value = aws_acm_certificate.domain_cert.domain_validation_options } locals { cf_origin_id = "yamor_pwa_cdn" } resource "aws_cloudfront_origin_access_control" "s3_oac" { name = "static_website" origin_access_control_origin_type = "s3" signing_behavior = "always" signing_protocol = "sigv4" } resource "aws_cloudfront_distribution" "cdn" { origin { domain_name = aws_s3_bucket.origin.bucket_regional_domain_name origin_id = local.cf_origin_id origin_access_control_id = aws_cloudfront_origin_access_control.s3_oac.id } enabled = true is_ipv6_enabled = true default_root_object = "index.html" http_version = "http2and3" aliases = [var.pwa_domain] viewer_certificate { acm_certificate_arn = aws_acm_certificate.domain_cert.arn ssl_support_method = "sni-only" } default_cache_behavior { # AWS が管理している CacheOptimized ポリシー cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" allowed_methods = ["GET", "HEAD", "OPTIONS"] cached_methods = ["GET", "HEAD"] target_origin_id = local.cf_origin_id compress = true viewer_protocol_policy = "redirect-to-https" min_ttl = 0 default_ttl = 31536000 max_ttl = 31536000 } restrictions { geo_restriction { locations = [] restriction_type = "none" } } } output "cloudfront_distribution_id" { value = aws_cloudfront_distribution.cdn.id } output "cloudfront_domain" { value = aws_cloudfront_distribution.cdn.domain_name }
-
-
-
@@ -0,0 +1,13 @@# SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> # SPDX-License-Identifier: AGPL-3.0-only variable "pwa_domain" { description = "PWAをホストする最終的なドメイン" type = string } variable "aws_region" { description = "デフォルトのリージョン" type = string default = "us-west-2" }
-
-
-
@@ -21,6 +21,7 @@ "@types/react-dom": "^19.0.2","@vitejs/plugin-react": "^4.3.4", "rollup-plugin-license": "^3.5.3", "typescript": "^5.7.2", "vite": "^6.0.2" "vite": "^6.0.2", "zod": "^3.24.1" } }
-