From 21373e037243f64fa1c8fd698e12c8227ce9b27b Mon Sep 17 00:00:00 2001 From: bdeshi Date: Fri, 17 May 2024 06:52:49 +0600 Subject: [PATCH] iac: add backend infrastructure terraform config --- .envrc.sample | 15 ++++ .gitignore | 34 ++++++++ .terraform-version | 1 + .terraform.lock.hcl | 71 +++++++++++++++++ provider.aws.acm.tf | 13 ++++ provider.aws.cloudfront.tf | 49 ++++++++++++ provider.aws.data.tf | 37 +++++++++ provider.aws.iam.tf | 19 +++++ provider.aws.s3.tf | 27 +++++++ provider.gitea.tf | 6 ++ provider.woodpecker.tf | 28 +++++++ terraform.backend.remote.tfvars.sample | 2 + terraform.outputs.tf | 40 ++++++++++ terraform.tf | 51 ++++++++++++ terraform.tfvars.sample | 5 ++ terraform.variables.tf | 103 +++++++++++++++++++++++++ terraforn.locals.tf | 9 +++ 17 files changed, 510 insertions(+) create mode 100644 .envrc.sample create mode 100644 .gitignore create mode 100644 .terraform-version create mode 100644 .terraform.lock.hcl create mode 100644 provider.aws.acm.tf create mode 100644 provider.aws.cloudfront.tf create mode 100644 provider.aws.data.tf create mode 100644 provider.aws.iam.tf create mode 100644 provider.aws.s3.tf create mode 100644 provider.gitea.tf create mode 100644 provider.woodpecker.tf create mode 100644 terraform.backend.remote.tfvars.sample create mode 100644 terraform.outputs.tf create mode 100644 terraform.tf create mode 100644 terraform.tfvars.sample create mode 100644 terraform.variables.tf create mode 100644 terraforn.locals.tf diff --git a/.envrc.sample b/.envrc.sample new file mode 100644 index 0000000..3ca1374 --- /dev/null +++ b/.envrc.sample @@ -0,0 +1,15 @@ +export TF_WORKSPACE=production + +export TF_TOKEN_app_terraform_io=*** +export TF_CLOUD_ORGANIZATION=*** +export TF_CLOUD_PROJECT=*** + +export AWS_PROFILE=*** +export AWS_ACCESS_KEY_ID=*** +export AWS_SECRET_ACCESS_KEY=*** + +export WOODPECKER_SERVER=*** +export WOODPECKER_TOKEN=*** + +export GITEA_BASE_URL=*** +export GITEA_TOKEN=*** diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6304eb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/.terraform-version b/.terraform-version new file mode 100644 index 0000000..a7ee35a --- /dev/null +++ b/.terraform-version @@ -0,0 +1 @@ +1.8.3 diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..c142f22 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,71 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/adduc/woodpecker" { + version = "0.4.0" + constraints = "~> 0.4.0" + hashes = [ + "h1:5eWAYuzqOvSRYBQd+PY6B6QehfVjCWVrmvGRZCuoy1g=", + "zh:05733feedb15dbe55fe0cd00524e0a2c711e702209165dd6e972cd03e01ab6c3", + "zh:10a992a8ae0ace49e3835dd5d351fe73bcab8c290cec4783df83a254bc989ee0", + "zh:234166ba5eef01fd57668849465bbee253520d40369304d94a02658681b6460b", + "zh:487d11f4bdcad15b5218beb458f4a8835bf4a4bf98c8061607b0bf04f9058201", + "zh:49a772c34b1f0ecc18601e61491ee39c4c626143858fa5f772f9bb1c8339ee2b", + "zh:7d1d8a1b7fdaf5611a6f3089ea223a64bf5ef7cda0083ff2d0c272800b016ae9", + "zh:856f411e63aadd802e795c931d1bd4ee3095554e45bfd7ee6899bdf3d8891256", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:8fbf4c6dd5b9a51b4b001488d5dd33b7da0e290ae8c2bdef70eefab613ca4a48", + "zh:9cf22b558fe59ae410be1d4878a7baa590a69d1500ea2a587205b210b83230b7", + "zh:a02554c67cdac9feb0c041087933f05f5d24d6e473088647abdac61c13b92df3", + "zh:ad41ed2cec4b20e321021f49b18cf3fbf160f47ddf9a985190790e0a0a83ffb9", + "zh:afd606da5b9c458ad8b4bc675f1572e14f8e544bf524423213ff7796277eddf2", + "zh:b1eb152d035612001c0e0d4391c78a679cf71c8572d8f85323022c4f3eafadb6", + "zh:fd87c4332e4765025cb26275b05a5e2a7c74ae5cdabdc56ca88ebcaed06be75a", + ] +} + +provider "registry.terraform.io/go-gitea/gitea" { + version = "0.3.0" + constraints = "~> 0.3.0" + hashes = [ + "h1:9kI/rtwDrBt0Km055WJswN+PeGegoEov+1ZmyQ3QxAA=", + "zh:37e9c35f76a5fa71b7864aa6af45c380463b5ea2afd162109f9960bf33f7b93e", + "zh:4496717687dea48de96db815def8b2144b46c5c8a885c139dd45d5ddc6d13f4e", + "zh:4875b3e9092d4f15678f7a605469c144bf298b05c8f8527bb27b1fdf6cb6fba0", + "zh:51f15e0ef905619eb7236bbbdebd81f70f5e024c025a347b0224ed95c5103668", + "zh:5779e9276a20c294710ec57397c06fb3afd9bffd28a5de8189fd7af1ed925ea9", + "zh:63c2ec086260a2e15c9e77ca49344a56e4b86d52b3f502941c9562aa12345887", + "zh:728fd15b2f3ec1c60ad45a996bac98022198078d0368507516f3a0526fd6c503", + "zh:7951a3bf904f836c73b00263d2f057f5ffc123c2946508a57ca2d2a1dc3874ab", + "zh:8495b9e6f6ae9f49b8e80fe3ccf47f1f942745c21fa30648e98aa6fe41a647d9", + "zh:862888963677516379a34c4dbb2396810e1a0ac2e644704d692e4f847d487f55", + "zh:8b1e1badf2ea6c4fcfdf71d98a68a8ba8f0850a4c5ec5f5a451a81cfd2c2b9e2", + "zh:936671c9700a8549b9a4540ecec167415db704e97744ca1fd5e3ad9d48020693", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:c134d5445ce56de7115ceb16d65ed2082b7987273a9d17626e9f6a4e6e8d4ce9", + "zh:fb6fc4d41737bf2e0bd4a2e40ae2d7bddcda7361968f6b74fad00b4fd55e9506", + ] +} + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.49.0" + constraints = "~> 5.49.0" + hashes = [ + "h1:Y3xvYjzBIwYSbcnZDcs6moiy30uxRoY5oT2ExQHKG5A=", + "zh:0979b07cdeffb868ea605e4bbc008adc7cccb5f3ba1d3a0b794ea3e8fff20932", + "zh:2121a0a048a1d9419df69f3561e524b7e8a6b74ba0f57bd8948799f12b6ad3a1", + "zh:573362042ba0bd18e98567a4f45d91b09eb0d223513518ba04f16a646a906403", + "zh:57be7a4d6c362be2fa586d270203f4eac1ee239816239a9503b86ebc8fa1fef0", + "zh:5c72ed211d9234edd70eac9d77c3cafc7bbf819d1c28332a6d77acf227c9a23c", + "zh:7786d1a9781f8e8c0079bf58f4ed4aeddec0caf54ad7ddcf43c47936d545a04f", + "zh:82133e7d39787ee91ed41988da71beecc2ecb900b5da94b3f3d77fbc4d4dc722", + "zh:8cdb1c154dead85be8352afd30eaf41c59249de9e7e0a8eb4ab8e625b90a4922", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ac215fd1c3bd647ae38868940651b97a53197688daefcd70b3595c84560e5267", + "zh:c45db22356d20e431639061a72e07da5201f4937c1df6b9f03f32019facf3905", + "zh:c9ba90e62db9a4708ed1a4e094849f88ce9d44c52b49f613b30bb3f7523b8d97", + "zh:d2be3607be2209995c80dc1d66086d527de5d470f73509e813254067e8287106", + "zh:e3fa20090f3cebf3911fc7ef122bd8c0505e3330ab7d541fa945fea861205007", + "zh:ef1b9d5c0b6279323f2ecfc322db8083e141984cfe1bb2f33c0f4934fccb69e3", + ] +} diff --git a/provider.aws.acm.tf b/provider.aws.acm.tf new file mode 100644 index 0000000..52220d7 --- /dev/null +++ b/provider.aws.acm.tf @@ -0,0 +1,13 @@ +resource "aws_acm_certificate" "created" { + domain_name = var.domain_name + validation_method = "DNS" + subject_alternative_names = [] + validation_option { + domain_name = var.domain_name + validation_domain = var.domain_name + } +} + +resource "aws_acm_certificate_validation" "created" { + certificate_arn = aws_acm_certificate.created.arn +} diff --git a/provider.aws.cloudfront.tf b/provider.aws.cloudfront.tf new file mode 100644 index 0000000..e9c893c --- /dev/null +++ b/provider.aws.cloudfront.tf @@ -0,0 +1,49 @@ +data "aws_cloudfront_cache_policy" "caching_optimized" { + name = "Managed-CachingOptimized" +} + +locals { + cloudfront_s3_origin_id = "s3origin" +} + +resource "aws_cloudfront_distribution" "created" { + enabled = true + is_ipv6_enabled = true + aliases = [var.domain_name] + default_root_object = var.aws_cloudfront_default_root_object + price_class = var.aws_cloudfront_price_class + http_version = "http2and3" + origin { + domain_name = aws_s3_bucket.created.bucket_regional_domain_name + origin_id = local.cloudfront_s3_origin_id + origin_access_control_id = aws_cloudfront_origin_access_control.s3_access.id + } + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id + target_origin_id = local.cloudfront_s3_origin_id + viewer_protocol_policy = "redirect-to-https" + compress = true + } + restrictions { + geo_restriction { + restriction_type = "none" + locations = [] + } + } + viewer_certificate { + acm_certificate_arn = aws_acm_certificate.created.arn + minimum_protocol_version = var.aws_cloudfront_minimum_protocol_version + ssl_support_method = "sni-only" + } + + depends_on = [aws_acm_certificate_validation.created] +} + +resource "aws_cloudfront_origin_access_control" "s3_access" { + name = "${var.domain_name}_s3" + origin_access_control_origin_type = "s3" + signing_behavior = "always" + signing_protocol = "sigv4" +} diff --git a/provider.aws.data.tf b/provider.aws.data.tf new file mode 100644 index 0000000..f705fbe --- /dev/null +++ b/provider.aws.data.tf @@ -0,0 +1,37 @@ +data "aws_caller_identity" "current" {} + +data "aws_iam_policy_document" "s3_cloudfront_access" { + statement { + principals { + type = "Service" + identifiers = ["cloudfront.amazonaws.com"] + } + actions = [ + "s3:GetObject", + "s3:ListBucket" + ] + resources = [ + aws_s3_bucket.created.arn, + "${aws_s3_bucket.created.arn}/*" + ] + condition { + test = "StringEquals" + variable = "AWS:SourceArn" + values = [aws_cloudfront_distribution.created.arn] + } + } +} + +data "aws_iam_policy_document" "pubilsher" { + statement { + actions = [ + "s3:*", + "cloudfront:*" + ] + resources = [ + aws_s3_bucket.created.arn, + "${aws_s3_bucket.created.arn}/*", + aws_cloudfront_distribution.created.arn + ] + } +} diff --git a/provider.aws.iam.tf b/provider.aws.iam.tf new file mode 100644 index 0000000..3b05b88 --- /dev/null +++ b/provider.aws.iam.tf @@ -0,0 +1,19 @@ +resource "aws_iam_user" "publisher" { + name = "${var.domain_name}_publisher" + path = "/${var.aws_tag_iac_identifier}/${local.workspace_env}/" + force_destroy = true +} + +resource "aws_iam_access_key" "publisher" { + user = aws_iam_user.publisher.name +} + +resource "aws_iam_policy" "publisher" { + name_prefix = "${var.domain_name}_publisher" + policy = data.aws_iam_policy_document.pubilsher.json +} + +resource "aws_iam_user_policy_attachment" "publisher" { + policy_arn = aws_iam_policy.publisher.arn + user = aws_iam_user.publisher.name +} diff --git a/provider.aws.s3.tf b/provider.aws.s3.tf new file mode 100644 index 0000000..86f41b4 --- /dev/null +++ b/provider.aws.s3.tf @@ -0,0 +1,27 @@ +resource "aws_s3_bucket" "created" { + bucket_prefix = var.aws_s3_use_domain_prefix ? var.domain_name : var.aws_s3_bucket_prefix + force_destroy = var.aws_s3_force_destroy +} + +resource "aws_s3_bucket_public_access_block" "created" { + bucket = aws_s3_bucket.created.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" "created" { + bucket = aws_s3_bucket.created.id + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "aws/s3" + sse_algorithm = "aws:kms" + } + } +} + +resource "aws_s3_bucket_policy" "created" { + bucket = aws_s3_bucket.created.id + policy = data.aws_iam_policy_document.s3_cloudfront_access.json +} diff --git a/provider.gitea.tf b/provider.gitea.tf new file mode 100644 index 0000000..8cf3da4 --- /dev/null +++ b/provider.gitea.tf @@ -0,0 +1,6 @@ +data "gitea_user" "current" {} + +data "gitea_repo" "source" { + name = var.gitea_repo + username = coalesce(var.gitea_user, data.gitea_user.current.username) +} diff --git a/provider.woodpecker.tf b/provider.woodpecker.tf new file mode 100644 index 0000000..c8e24a2 --- /dev/null +++ b/provider.woodpecker.tf @@ -0,0 +1,28 @@ +locals { + secrets_map = { + aws_region = { value = var.aws_region } + aws_access_key_id = { value = aws_iam_access_key.publisher.id } + aws_secret_access_key = { value = aws_iam_access_key.publisher.secret } + cloudfront_distribution = { value = aws_cloudfront_distribution.created.id } + s3_bucket = { value = aws_s3_bucket.created.id } + } +} + +data "woodpecker_self" "current" {} + +resource "woodpecker_repository" "created" { + name = data.gitea_repo.source.name + # woodpecker username can come from associated gitea username + owner = coalesce(var.woodpecker_user, var.gitea_user, data.woodpecker_self.current.login) + visibility = data.gitea_repo.source.private ? "Public" : "Private" +} + +resource "woodpecker_repository_secret" "secrets" { + count = length(keys(local.secrets_map)) + + repo_owner = woodpecker_repository.created.owner + repo_name = woodpecker_repository.created.name + name = upper(keys(local.secrets_map)[count.index]) + value = values(local.secrets_map)[count.index].value + events = try(values(local.secrets_map)[count.index].events, var.woodpecker_secrets_events, []) +} diff --git a/terraform.backend.remote.tfvars.sample b/terraform.backend.remote.tfvars.sample new file mode 100644 index 0000000..255763f --- /dev/null +++ b/terraform.backend.remote.tfvars.sample @@ -0,0 +1,2 @@ +organization = "bdeshi-space" +workspaces { prefix = "resume-manpage-" } diff --git a/terraform.outputs.tf b/terraform.outputs.tf new file mode 100644 index 0000000..9dab68e --- /dev/null +++ b/terraform.outputs.tf @@ -0,0 +1,40 @@ +output "aws_account_id" { + value = data.aws_caller_identity.current.account_id + description = "ID of the AWS account." +} + +output "s3_bucket" { + value = aws_s3_bucket.created.id + description = "name of the created S3 bucket." +} + +output "cloudfront_distribution" { + value = aws_cloudfront_distribution.created.id + description = "ID of the created CloudFront distribution." +} + +output "acm_certificate_arn" { + value = aws_acm_certificate.created.arn + description = "ARN of the created ACM certificate." +} + +output "acm_validation_options" { + value = aws_acm_certificate.created.domain_validation_options + description = "ACM domain validation records." +} + +output "iam_access_key_id" { + value = aws_iam_access_key.publisher.id + description = "access key ID of the publisher IAM user." +} + +output "iam_secret_access_key" { + value = aws_iam_access_key.publisher.secret + sensitive = true + description = "secret access key of the publisher IAM user." +} + +output "domain_name" { + value = var.domain_name + description = "target publishing domain name." +} diff --git a/terraform.tf b/terraform.tf new file mode 100644 index 0000000..9a391f4 --- /dev/null +++ b/terraform.tf @@ -0,0 +1,51 @@ +terraform { + required_version = "~> 1.8.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~>5.49.0" + } + woodpecker = { + source = "adduc/woodpecker" + version = "~> 0.4.0" + } + gitea = { + source = "go-gitea/gitea" + version = "~>0.3.0" + } + } + + backend "remote" {} + + # cloud { + # organization = collected from TF_CLOUD_ORGANIZATION env + # workspaces { + # project = collected from TF_CLOUD_PROJECT env + # } + # } + +} + +provider "aws" { + # profile = collected from AWS_PROFILE env + region = var.aws_region + default_tags { + tags = { + "ManagedBy" = var.aws_tag_iac_identifier + "iac/project" = var.aws_tag_iac_project_name + "iac/source" = "${data.gitea_repo.source.ssh_url}/${var.aws_tag_iac_project_subpath}" + "iac/environment" = local.workspace_env + } + } +} + +provider "woodpecker" { + # server = collected from WOODPECKER_SERVER env + # token = collected from WOODPECKER_TOKEN env +} + +provider "gitea" { + # base_url = collected from GITEA_BASE_URL env + # token = collected from GITEA_TOKEN env +} diff --git a/terraform.tfvars.sample b/terraform.tfvars.sample new file mode 100644 index 0000000..78ebd9f --- /dev/null +++ b/terraform.tfvars.sample @@ -0,0 +1,5 @@ +domain_name = "sammay.sarkar.website" +aws_tag_iac_project_name = "resume-manpage" +aws_tag_iac_project_subpath = "/iac" +gitea_repo = "resume-manpage" +woodpecker_secrets_events = ["push", "deployment", "manual"] diff --git a/terraform.variables.tf b/terraform.variables.tf new file mode 100644 index 0000000..619a23c --- /dev/null +++ b/terraform.variables.tf @@ -0,0 +1,103 @@ +#################### +#### commons #### +#################### + +variable "domain_name" { + type = string + description = "domain name where the built site is published." +} + + +################ +#### aws #### +################ + +variable "aws_tag_iac_identifier" { + type = string + default = "iac/terraform" + description = "IaC tool name added as a tag to AWS resources, also used in iam user path." +} + +variable "aws_tag_iac_project_name" { + type = string + description = "IaC project name added as a tag to AWS resources." +} + +variable "aws_tag_iac_project_subpath" { + type = string + description = "IaC project source path added as a tag to AWS resources." +} + +variable "aws_region" { + type = string + default = "us-east-1" + description = "AWS region passed to AWS provider." +} + +variable "aws_s3_bucket_prefix" { + type = string + default = null + description = "AWS S3 bucket name prefix." +} + +variable "aws_s3_use_domain_prefix" { + type = bool + default = true + description = "use var.domain_name as AWS S3 bucket name prefix." +} + +variable "aws_s3_force_destroy" { + type = bool + default = true + description = "delete all bucket objects to allow clean bucket destroy operation." +} + +variable "aws_cloudfront_default_root_object" { + type = string + default = "index.html" + description = "default root object name for the CloudFront distribution." +} + +variable "aws_cloudfront_price_class" { + type = string + default = "PriceClass_200" + description = "price class for the CloudFront distribution: PriceClass_All|PriceClass_200|PriceClass_100." +} + +variable "aws_cloudfront_minimum_protocol_version" { + type = string + default = "TLSv1.2_2021" + description = "name of the minimum SSL protocol version used by CloudFront for HTTPS requests." +} + +################ +#### gitea #### +################ + +variable "gitea_repo" { + type = string + description = "name of source Gitea repository." +} + +variable "gitea_user" { + type = string + default = null + description = "username of Gitea repo owner." +} + + +################ +# woodpecker # +################ + +variable "woodpecker_user" { + type = string + default = null + description = "username of Woodpecker server." +} + +variable "woodpecker_secrets_events" { + type = list(string) + default = ["push"] + description = "default list of allowed events for Woodpecker secrets created." +} diff --git a/terraforn.locals.tf b/terraforn.locals.tf new file mode 100644 index 0000000..1f6808e --- /dev/null +++ b/terraforn.locals.tf @@ -0,0 +1,9 @@ +locals { + # terraform remote backend prefix key means local and remote + # wokspace names can differ. + # assuming workspace are named as `prefix+env`, this section + # extracts the env from both local or remote workspace names. + _workspace_name_segments = split("-", terraform.workspace) + _workspace_name_segments_count = length(local._workspace_name_segments) + workspace_env = local._workspace_name_segments[local._workspace_name_segments_count - 1] +}