commit 21373e037243f64fa1c8fd698e12c8227ce9b27b Author: bdeshi Date: Fri May 17 06:52:49 2024 +0600 iac: add backend infrastructure terraform config 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] +}