commit 210c454359f6c3fa2f27663758b204cb9f5426b8 Author: kappa Date: Tue Sep 9 09:08:17 2025 +0900 Initial commit: AWS CloudFront with OpenTofu infrastructure - Complete CloudFront distribution setup with origin.servidor.it.com - WAF v2 integration for security protection - S3 backend for Terraform state management - CloudFront logging to S3 - HTTP-only origin protocol configuration (resolves 504 Gateway Timeout) - Comprehensive documentation with deployment guide πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e64dc5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Terraform/OpenTofu files +*.tfstate +*.tfstate.* +*.tfvars +!terraform.tfvars.example +.terraform/ +.terraform.lock.hcl + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +*.log + +# Temporary files +*.tmp +*.temp + +# AWS credentials +.aws/ +credentials +config + +# S3 backend files (already stored remotely) +terraform.tfstate +terraform.tfstate.backup \ No newline at end of file diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..456cb7a --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -0,0 +1,241 @@ +# AWS CloudFront 배포 κ°€μ΄λ“œ + +## πŸ“‹ ν”„λ‘œμ νŠΈ κ°œμš” +AWS CloudFront CDN을 OpenTofu(Terraform fork)둜 κ΅¬μ„±ν•˜μ—¬ `origin.servidor.it.com`을 origin으둜 μ‚¬μš©ν•˜λŠ” μΈν”„λΌμŠ€νŠΈλŸ­μ²˜ ν”„λ‘œμ νŠΈμž…λ‹ˆλ‹€. + +## πŸ—οΈ ν˜„μž¬ 인프라 μƒνƒœ + +### CloudFront Distribution +- **Distribution ID**: E1XR8P4ENGP8RU +- **CloudFront URL**: https://dspki4yrh5oy1.cloudfront.net +- **Origin**: origin.servidor.it.com (HTTP-only) +- **Status**: βœ… Deployed and Working + +### λ³΄μ•ˆ μ„€μ • +- **WAF**: AWS WAF v2 ν™œμ„±ν™” + - WAF ID: d21d84c1-edb9-40af-9cdd-27f42f09c499 + - Rate Limiting: 10,000 requests/5min per IP + - AWS Managed Rules - Common Rule Set (SQL Injection, XSS λ°©μ–΄) + - AWS Managed Rules - Known Bad Inputs (μ•…μ„± νŒ¨ν„΄ 차단) +- **Viewer Protocol**: HTTPS (redirect-to-https) +- **Origin Protocol**: HTTP-only (μ€‘μš”: HTTPS μ‚¬μš© μ‹œ 504 μ—λŸ¬) + +### λ‘œκΉ… 및 μƒνƒœ 관리 +- **CloudFront Logs**: `s3://aws-cf-cloudfront-logs-535294143817/cloudfront-logs/` +- **Terraform State**: `s3://aws-cf-terraform-state-535294143817/aws-cf/terraform.tfstate` +- **State Locking**: λΉ„ν™œμ„±ν™” (DynamoDB κΆŒν•œ μ—†μŒ) + +## πŸš€ 배포 방법 + +### 사전 μš”κ΅¬μ‚¬ν•­ +- OpenTofu λ˜λŠ” Terraform μ„€μΉ˜ +- AWS CLI ꡬ성 +- ν•„μš”ν•œ IAM κΆŒν•œ: + - CloudFrontFullAccess + - S3FullAccess + - Route53FullAccess + - AWSCertificateManagerFullAccess + - AWSWAFFullAccess (WAFv2 포함) + - AWSCloudFormationFullAccess + +### 초기 μ„€μ • +```bash +# S3 Backend μ„€μ • (이미 μ™„λ£Œλ¨) +./setup-backend.sh + +# State λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ (이미 μ™„λ£Œλ¨) +echo "yes" | tofu init -migrate-state +``` + +### 배포 λͺ…λ Ήμ–΄ +```bash +# μ΄ˆκΈ°ν™” +tofu init + +# κ³„νš 확인 +tofu plan + +# 배포 μ‹€ν–‰ +tofu apply -auto-approve + +# μƒνƒœ 확인 +tofu state list +``` + +## πŸ“ ν”„λ‘œμ νŠΈ ꡬ쑰 +``` +aws-cf/ +β”œβ”€β”€ main.tf # CloudFront 메인 ꡬ성 +β”œβ”€β”€ variables.tf # λ³€μˆ˜ μ •μ˜ +β”œβ”€β”€ terraform.tfvars # λ³€μˆ˜ κ°’ μ„€μ • +β”œβ”€β”€ outputs.tf # 좜λ ₯ μ •μ˜ +β”œβ”€β”€ versions.tf # Provider 버전 관리 +β”œβ”€β”€ backend.tf # S3 Backend μ„€μ • +β”œβ”€β”€ security.tf # WAF 및 λ³΄μ•ˆ κ·Έλ£Ή +β”œβ”€β”€ acm.tf # ACM μΈμ¦μ„œ (λΉ„ν™œμ„±ν™”) +β”œβ”€β”€ setup-backend.sh # S3 Backend μ„€μ • 슀크립트 +β”œβ”€β”€ README.md # κΈ°μ‘΄ λ¬Έμ„œ +└── DEPLOYMENT_GUIDE.md # 이 λ¬Έμ„œ +``` + +## βš™οΈ μ€‘μš” μ„€μ • λ‚΄μš© + +### terraform.tfvars 핡심 μ„€μ • +```hcl +# Origin μ„€μ • (맀우 μ€‘μš”) +origin_domain = "origin.servidor.it.com" +origin_protocol_policy = "http-only" # HTTPS μ‚¬μš© μ‹œ 504 μ—λŸ¬! + +# CloudFront μ„€μ • +viewer_protocol_policy = "redirect-to-https" +price_class = "PriceClass_100" + +# λ³΄μ•ˆ μ„€μ • +enable_waf = true + +# λ‘œκΉ… μ„€μ • +enable_cloudfront_logging = true +cloudfront_logs_bucket = "aws-cf-cloudfront-logs-535294143817" + +# λΉ„ν™œμ„±ν™”λœ κΈ°λŠ₯ (κΆŒν•œ/μ œν•œ 사항) +create_route53_records = false # CAA μ œν•œμœΌλ‘œ λΉ„ν™œμ„±ν™” +create_acm_certificate = false # CloudFront κΈ°λ³Έ μΈμ¦μ„œ μ‚¬μš© +enable_cloudformation_stack = false # CloudFormation κΆŒν•œ μ—†μŒ +``` + +## πŸ”§ 문제 ν•΄κ²° νžˆμŠ€ν† λ¦¬ + +### 1. 504 Gateway Timeout ν•΄κ²° βœ… +**문제**: CloudFrontκ°€ origin μ„œλ²„μ— μ—°κ²°ν•  수 μ—†μŒ +**원인**: origin.servidor.it.com이 HTTP(80)만 μ§€μ›ν•˜λŠ”λ° HTTPS(443)둜 μ—°κ²° μ‹œλ„ +**ν•΄κ²°**: +```hcl +origin_protocol_policy = "http-only" +``` + +### 2. ACM μΈμ¦μ„œ CAA μ œν•œ βœ… +**문제**: *.servidor.it.com 도메인에 λŒ€ν•œ ACM μΈμ¦μ„œ λ°œκΈ‰ μ‹€νŒ¨ +**원인**: CAA DNS λ ˆμ½”λ“œκ°€ AWS μΈμ¦μ„œ λ°œκΈ‰μ„ μ œν•œ +**ν•΄κ²°**: CloudFront κΈ°λ³Έ μΈμ¦μ„œ μ‚¬μš© +```hcl +create_acm_certificate = false +viewer_certificate { + cloudfront_default_certificate = true +} +``` + +### 3. DynamoDB State Locking κΆŒν•œ βœ… +**문제**: Terraform state locking을 μœ„ν•œ DynamoDB ν…Œμ΄λΈ” 생성/μ ‘κ·Ό λΆˆκ°€ +**ν•΄κ²°**: S3만 μ‚¬μš© (1인 개발 μ‹œ μΆ©λΆ„) +```hcl +backend "s3" { + bucket = "aws-cf-terraform-state-535294143817" + key = "aws-cf/terraform.tfstate" + # dynamodb_table = "terraform-state-lock" # λΉ„ν™œμ„±ν™” +} +``` + +### 4. 쀑볡 CloudFront Distribution 정리 βœ… +**문제**: μ—¬λŸ¬ 배포 μ‹œλ„λ‘œ 3개의 distribution 생성 +**ν•΄κ²°**: +- E18GW141CX7I8C - λΉ„ν™œμ„±ν™” 쀑 (μ‚­μ œ μ˜ˆμ •) +- E32FD742KMW2YY - λΉ„ν™œμ„±ν™” 쀑 (μ‚­μ œ μ˜ˆμ •) +- E1XR8P4ENGP8RU - ν˜„μž¬ μ‚¬μš© 쀑 βœ… + +### 5. WAF κΆŒν•œ 문제 βœ… +**초기 문제**: WAFv2 κΆŒν•œ μ—†λ‹€κ³  였λ₯˜ λ°œμƒ +**ν•΄κ²°**: AWSWAFFullAccess 정책에 wafv2:* κΆŒν•œ ν¬ν•¨λ˜μ–΄ 있음 확인 + +## πŸ“Š λͺ¨λ‹ˆν„°λ§ 및 관리 + +### μƒνƒœ 확인 λͺ…λ Ήμ–΄ +```bash +# CloudFront μƒνƒœ +aws cloudfront get-distribution --id E1XR8P4ENGP8RU \ + --query 'Distribution.Status' --output text + +# WAF μ—°κ²° 확인 +aws cloudfront get-distribution --id E1XR8P4ENGP8RU \ + --query 'Distribution.DistributionConfig.WebACLId' --output text + +# 둜그 확인 +aws s3 ls s3://aws-cf-cloudfront-logs-535294143817/cloudfront-logs/ --recursive + +# State 파일 확인 +aws s3 ls s3://aws-cf-terraform-state-535294143817/aws-cf/ +``` + +### μ„±λŠ₯ ν…ŒμŠ€νŠΈ +```bash +# CloudFront 응닡 확인 +curl -I https://dspki4yrh5oy1.cloudfront.net + +# 좜λ ₯ μ˜ˆμ‹œ: +# HTTP/2 200 +# content-type: text/html +# x-cache: Miss from cloudfront (첫 μš”μ²­) / Hit from cloudfront (μΊμ‹œλœ μš”μ²­) +``` + +### λΉ„ν™œμ„±ν™”λœ Distribution μ‚­μ œ (μ™„μ „ λΉ„ν™œμ„±ν™” ν›„) +```bash +# μƒνƒœ 확인 +aws cloudfront list-distributions \ + --query 'DistributionList.Items[*].[Id,Status,DistributionConfig.Enabled]' \ + --output table + +# μ‚­μ œ (Deployed μƒνƒœμ—μ„œ Enabled=false 일 λ•Œ) +aws cloudfront get-distribution-config --id E18GW141CX7I8C > dist-config.json +ETAG=$(jq -r '.ETag' dist-config.json) +aws cloudfront delete-distribution --id E18GW141CX7I8C --if-match $ETAG +``` + +## πŸ”’ λ³΄μ•ˆ 고렀사항 + +1. **WAF μ„€μ •** + - ν˜„μž¬ κΈ°λ³Έ AWS Managed Rules μ‚¬μš© + - ν•„μš”μ‹œ μ»€μŠ€ν…€ λ£° μΆ”κ°€ κ°€λŠ₯ + - Rate limiting μ‘°μ • κ°€λŠ₯ (security.tf) + +2. **Origin λ³΄μ•ˆ** + - ⚠️ Origin이 HTTP만 μ§€μ›ν•˜λ―€λ‘œ CloudFront-Origin κ°„ νŠΈλž˜ν”½ μ•”ν˜Έν™” μ•ˆλ¨ + - ꢌμž₯: Origin μ„œλ²„μ— HTTPS 지원 μΆ”κ°€ + +3. **둜그 관리** + - CloudFront 둜그 90일 μžλ™ μ‚­μ œ μ„€μ • + - μž₯κΈ° 보관 ν•„μš”μ‹œ S3 lifecycle μˆ˜μ • + +## πŸ“ ν–₯ν›„ κ°œμ„  사항 + +### κΆŒν•œ νšλ“ μ‹œ ν™œμ„±ν™” κ°€λŠ₯ν•œ κΈ°λŠ₯ +1. **DynamoDB State Locking** + - νŒ€ ν˜‘μ—… μ‹œ ν•„μˆ˜ + - backend.tfμ—μ„œ 주석 ν•΄μ œ + +2. **Custom Domain with ACM** + - CAA λ ˆμ½”λ“œ μˆ˜μ • ν›„ κ°€λŠ₯ + - terraform.tfvarsμ—μ„œ `create_acm_certificate = true` + +3. **CloudFormation Stack** + - VPC 및 λ„€νŠΈμ›Œν‚Ή λ¦¬μ†ŒμŠ€ 관리 + - terraform.tfvarsμ—μ„œ `enable_cloudformation_stack = true` + +### μ„±λŠ₯ μ΅œμ ν™” +1. Cache Policy μ΅œμ ν™” +2. Origin Request Policy μ‘°μ • +3. CloudFront Edge Location 선택 (Price Class) + +## 🏷️ νƒœκ·Έ 및 메타데이터 +- **Project**: aws-cf +- **Environment**: dev +- **ManagedBy**: OpenTofu +- **Owner**: kaffa +- **Created**: 2025-09-08 +- **LastUpdated**: 2025-09-09 + +## πŸ“š μ°Έκ³  자료 +- [OpenTofu Documentation](https://opentofu.org/docs/) +- [AWS CloudFront Best Practices](https://docs.aws.amazon.com/cloudfront/latest/developerguide/best-practices.html) +- [AWS WAF Documentation](https://docs.aws.amazon.com/waf/latest/developerguide/) + +--- +*이 λ¬Έμ„œλŠ” μ‹€μ œ 배포 κ²½ν—˜μ„ λ°”νƒ•μœΌλ‘œ μž‘μ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6525499 --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# AWS CloudFront with OpenTofu + +이 ν”„λ‘œμ νŠΈλŠ” OpenTofuλ₯Ό μ‚¬μš©ν•˜μ—¬ AWS CloudFront 배포와 CloudFormation μŠ€νƒμ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€. + +## ꡬ쑰 + +- `versions.tf` - Provider 및 Terraform 버전 μ„€μ • +- `variables.tf` - μž…λ ₯ λ³€μˆ˜ μ •μ˜ +- `main.tf` - CloudFront 배포 및 CloudFormation μŠ€νƒ +- `security.tf` - λ³΄μ•ˆ κ·Έλ£Ή 및 WAF μ„€μ • +- `outputs.tf` - 좜λ ₯ λ³€μˆ˜ +- `terraform.tfvars.example` - λ³€μˆ˜ μ„€μ • μ˜ˆμ‹œ + +## μ£Όμš” κΈ°λŠ₯ + +### CloudFront 배포 +- `origin.servidor.it.com`을 원본 μ„œλ²„λ‘œ μ‚¬μš© +- HTTPS λ¦¬λ””λ ‰μ…˜ 및 μ••μΆ• 지원 +- μ‚¬μš©μž μ •μ˜ 도메인(CNAME) 지원 +- μΊμ‹œ μ •μ±… 및 원본 μš”μ²­ μ •μ±… μ„€μ • +- μ»€μŠ€ν…€ 였λ₯˜ νŽ˜μ΄μ§€ μ„€μ • + +### CloudFormation μŠ€νƒ +- VPC 및 λ„€νŠΈμ›Œν‚Ή λ¦¬μ†ŒμŠ€ 생성 +- 퍼블릭 μ„œλΈŒλ„· 및 인터넷 κ²Œμ΄νŠΈμ›¨μ΄ +- μŠ€νƒ 좜λ ₯을 ν†΅ν•œ λ¦¬μ†ŒμŠ€ ID 곡유 + +### λ³΄μ•ˆ κΈ°λŠ₯ +- ALB 및 μ›Ή μ„œλ²„μš© λ³΄μ•ˆ κ·Έλ£Ή (선택사항) +- AWS WAF v2 μ›Ή ACL (선택사항) +- 레이트 μ œν•œ 및 관리 κ·œμΉ™ μ„ΈνŠΈ + +## μ‚¬μš© 방법 + +### 1. ν™˜κ²½ μ„€μ • + +```bash +# OpenTofu μ„€μΉ˜ 확인 +tofu version + +# AWS 자격 증λͺ… μ„€μ • +export AWS_ACCESS_KEY_ID="your-access-key" +export AWS_SECRET_ACCESS_KEY="your-secret-key" +export AWS_DEFAULT_REGION="us-east-1" +``` + +### 2. λ³€μˆ˜ μ„€μ • + +```bash +# μ„€μ • 파일 볡사 +cp terraform.tfvars.example terraform.tfvars + +# ν•„μš”ν•œ κ°’λ“€ μˆ˜μ • +vim terraform.tfvars +``` + +### 3. 배포 + +```bash +# μ΄ˆκΈ°ν™” +tofu init + +# κ³„νš 확인 +tofu plan + +# 배포 μ‹€ν–‰ +tofu apply +``` + +### 4. 확인 + +배포 ν›„ 좜λ ₯된 CloudFront URL을 톡해 μ„œλΉ„μŠ€μ— μ ‘κ·Όν•  수 μžˆμŠ΅λ‹ˆλ‹€: + +```bash +# CloudFront 배포 μƒνƒœ 확인 +aws cloudfront get-distribution --id + +# CloudFormation μŠ€νƒ μƒνƒœ 확인 +aws cloudformation describe-stacks --stack-name +``` + +## μ€‘μš” μ„€μ • + +### SSL μΈμ¦μ„œ + +이 섀정은 **ACM(AWS Certificate Manager) μΈμ¦μ„œ**λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€: + +- `servidor.it.com`κ³Ό `*.servidor.it.com` 도메인 지원 +- μžλ™μœΌλ‘œ us-east-1 리전에 ACM μΈμ¦μ„œ 생성 +- DNS 검증을 ν†΅ν•œ μžλ™ μΈμ¦μ„œ λ°œκΈ‰ +- Route53을 ν†΅ν•œ μžλ™ DNS λ ˆμ½”λ“œ 관리 +- CloudFront와 μ™„μ „ ν†΅ν•©λœ HTTPS μ„€μ • + +### μΊμ‹œ μ •μ±… + +κΈ°λ³Έ μ œκ³΅λ˜λŠ” AWS 관리 μ •μ±…: + +- `4135ea2d-6df8-44a3-9df3-4b5a84be39ad` - CachingDisabled +- `725fe1d6-8a84-4f3e-8ab1-bc2d5bb10e12` - CachingOptimized +- `df3c6b6c-4e41-4d9c-a8c7-6b8b2b8e6b8b` - CachingOptimizedForUncompressedObjects + +### λ³΄μ•ˆ 고렀사항 + +ν”„λ‘œλ•μ…˜ ν™˜κ²½μ—μ„œλŠ”: + +- `ssh_allowed_cidrs`λ₯Ό νŠΉμ • IP둜 μ œν•œ +- WAF ν™œμ„±ν™” (`enable_waf = true`) +- μ μ ˆν•œ μΊμ‹œ μ •μ±… 선택 +- CloudTrail 및 CloudWatch λ‘œκΉ… ν™œμ„±ν™” + +### 도메인 μ„€μ • + +**μ „μ œ 쑰건**: +- `servidor.it.com` λ„λ©”μΈμ˜ Route53 ν˜ΈμŠ€νŒ… μ˜μ—­μ΄ μ‘΄μž¬ν•΄μ•Ό 함 +- λ„λ©”μΈμ˜ λ„€μž„μ„œλ²„κ°€ Route53으둜 μ„€μ •λ˜μ–΄ μžˆμ–΄μ•Ό 함 + +**μžλ™ μ„€μ •**: +- ACM μΈμ¦μ„œ μžλ™ 생성 및 검증 +- Route53 A λ ˆμ½”λ“œ μžλ™ 생성 (`servidor.it.com` β†’ CloudFront) +- Route53 A λ ˆμ½”λ“œ μžλ™ 생성 (`www.servidor.it.com` β†’ CloudFront) + +## 좜λ ₯ 정보 + +배포 μ™„λ£Œ ν›„ λ‹€μŒ 정보듀이 좜λ ₯λ©λ‹ˆλ‹€: + +- CloudFront 배포 ID 및 도메인 +- CloudFormation μŠ€νƒ 정보 +- λ³΄μ•ˆ κ·Έλ£Ή ID (ν™œμ„±ν™”λœ 경우) +- WAF Web ACL ARN (ν™œμ„±ν™”λœ 경우) +- VPC 및 μ„œλΈŒλ„· 정보 + +## 정리 + +```bash +# λ¦¬μ†ŒμŠ€ μ‚­μ œ +tofu destroy +``` + +## 문제 ν•΄κ²° + +### 일반적인 였λ₯˜ + +1. **μΈμ¦μ„œ 였λ₯˜**: CloudFront용 μΈμ¦μ„œλŠ” us-east-1μ—μ„œλ§Œ 생성 κ°€λŠ₯ +2. **도메인 검증**: CNAME μ„€μ • 전에 도메인 μ†Œμœ κΆŒ 확인 ν•„μš” +3. **원본 μ„œλ²„**: `origin.servidor.it.com`이 HTTPSλ₯Ό μ§€μ›ν•˜λŠ”μ§€ 확인 + +### 둜그 확인 + +- CloudFront μ•‘μ„ΈμŠ€ 둜그 ν™œμ„±ν™” +- CloudWatch λ©”νŠΈλ¦­ λͺ¨λ‹ˆν„°λ§ +- WAF 둜그 뢄석 (ν™œμ„±ν™”λœ 경우) \ No newline at end of file diff --git a/acm.tf b/acm.tf new file mode 100644 index 0000000..7a42e45 --- /dev/null +++ b/acm.tf @@ -0,0 +1,82 @@ +# Data source to get Route53 hosted zone +data "aws_route53_zone" "main" { + count = var.create_route53_records ? 1 : 0 + name = var.domain_name + private_zone = false +} + +# ACM Certificate for CloudFront (must be in us-east-1) - conditional +resource "aws_acm_certificate" "main" { + count = var.create_acm_certificate ? 1 : 0 + provider = aws.us_east_1 + domain_name = var.domain_name + subject_alternative_names = ["*.${var.domain_name}"] + validation_method = var.certificate_domain_validation_options + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${var.project_name}-${var.environment}-certificate" + } +} + +# Route53 records for ACM certificate validation +resource "aws_route53_record" "cert_validation" { + provider = aws.us_east_1 + for_each = var.create_route53_records && var.create_acm_certificate ? { + for dvo in aws_acm_certificate.main[0].domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } : {} + + zone_id = data.aws_route53_zone.main[0].zone_id + name = each.value.name + type = each.value.type + records = [each.value.record] + ttl = 60 + allow_overwrite = true +} + +# ACM Certificate validation +resource "aws_acm_certificate_validation" "main" { + count = var.create_acm_certificate ? 1 : 0 + provider = aws.us_east_1 + certificate_arn = aws_acm_certificate.main[0].arn + validation_record_fqdns = var.create_route53_records ? [for record in aws_route53_record.cert_validation : record.fqdn] : null + + timeouts { + create = "10m" + } +} + +# Route53 A record for main domain (CloudFront alias) +resource "aws_route53_record" "main" { + count = var.create_route53_records ? 1 : 0 + zone_id = data.aws_route53_zone.main[0].zone_id + name = var.domain_name + type = "A" + + alias { + name = aws_cloudfront_distribution.main.domain_name + zone_id = aws_cloudfront_distribution.main.hosted_zone_id + evaluate_target_health = false + } +} + +# Route53 A record for www subdomain (CloudFront alias) +resource "aws_route53_record" "www" { + count = var.create_route53_records ? 1 : 0 + zone_id = data.aws_route53_zone.main[0].zone_id + name = "www.${var.domain_name}" + type = "A" + + alias { + name = aws_cloudfront_distribution.main.domain_name + zone_id = aws_cloudfront_distribution.main.hosted_zone_id + evaluate_target_health = false + } +} \ No newline at end of file diff --git a/backend.tf b/backend.tf new file mode 100644 index 0000000..c88e59b --- /dev/null +++ b/backend.tf @@ -0,0 +1,15 @@ +# S3 Backend Configuration for Terraform State +# This file configures remote state storage in S3 with DynamoDB for state locking + +terraform { + backend "s3" { + bucket = "aws-cf-terraform-state-535294143817" + key = "aws-cf/terraform.tfstate" + region = "us-east-1" + encrypt = true + # dynamodb_table = "terraform-state-lock" # Disabled due to permission issues + + # Optional: Add versioning for state file history + # versioning = true + } +} \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..228c261 --- /dev/null +++ b/main.tf @@ -0,0 +1,224 @@ +# CloudFront Origin Access Control +resource "aws_cloudfront_origin_access_control" "oac" { + name = "${var.project_name}-${var.environment}-oac" + description = "OAC for ${var.project_name}" + origin_access_control_origin_type = "s3" + signing_behavior = "always" + signing_protocol = "sigv4" +} + +# CloudFront Distribution +resource "aws_cloudfront_distribution" "main" { + # Origin configuration for custom domain + origin { + domain_name = var.origin_domain + origin_id = "${var.project_name}-${var.environment}-origin" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = var.origin_protocol_policy + origin_ssl_protocols = ["TLSv1.2"] + origin_read_timeout = 30 + origin_keepalive_timeout = 5 + } + + # Custom headers (optional) + # custom_header { + # name = "User-Agent" + # value = "CloudFront-${var.project_name}" + # } + } + + enabled = true + is_ipv6_enabled = true + comment = "CloudFront distribution for ${var.project_name} - ${var.environment}" + default_root_object = "index.html" + + # Aliases (custom domain names) - Disabled for default certificate + # aliases = var.cloudfront_aliases + + # Default cache behavior + default_cache_behavior { + allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "${var.project_name}-${var.environment}-origin" + cache_policy_id = var.cache_policy_id + origin_request_policy_id = var.origin_request_policy_id + viewer_protocol_policy = var.viewer_protocol_policy + compress = true + + # Forward headers, query strings, and cookies + # Use cache policies instead for better performance + } + + # Ordered cache behaviors (optional) + ordered_cache_behavior { + path_pattern = "/api/*" + allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "${var.project_name}-${var.environment}-origin" + cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # CachingDisabled + viewer_protocol_policy = "https-only" + compress = true + } + + # Price class + price_class = var.price_class + + # Geographic restrictions + restrictions { + geo_restriction { + restriction_type = "none" + # locations = [] + } + } + + # SSL/TLS certificate - Use CloudFront default certificate (temporary) + viewer_certificate { + cloudfront_default_certificate = true + } + + # Custom error responses + custom_error_response { + error_code = 404 + response_code = 200 + response_page_path = "/index.html" + error_caching_min_ttl = 300 + } + + custom_error_response { + error_code = 403 + response_code = 200 + response_page_path = "/index.html" + error_caching_min_ttl = 300 + } + + # Web Application Firewall (optional) + web_acl_id = var.enable_waf ? aws_wafv2_web_acl.cloudfront[0].arn : null + + # Logging configuration + dynamic "logging_config" { + for_each = var.enable_cloudfront_logging ? [1] : [] + content { + bucket = "${var.cloudfront_logs_bucket}.s3.amazonaws.com" + prefix = var.cloudfront_logs_prefix + include_cookies = false + } + } + + tags = { + Name = "${var.project_name}-${var.environment}-distribution" + Origin = var.origin_domain + } +} + +# CloudFormation Stack (optional - for additional AWS resources) +resource "aws_cloudformation_stack" "network" { + count = var.enable_cloudformation_stack ? 1 : 0 + name = "${var.project_name}-${var.environment}-network-stack" + + parameters = { + VPCCidr = "10.0.0.0/16" + Environment = var.environment + ProjectName = var.project_name + } + + template_body = jsonencode({ + AWSTemplateFormatVersion = "2010-09-09" + Description = "Network resources for ${var.project_name}" + + Parameters = { + VPCCidr = { + Type = "String" + Default = "10.0.0.0/16" + Description = "CIDR block for the VPC" + } + Environment = { + Type = "String" + Description = "Environment name" + } + ProjectName = { + Type = "String" + Description = "Project name" + } + } + + Resources = { + VPC = { + Type = "AWS::EC2::VPC" + Properties = { + CidrBlock = { Ref = "VPCCidr" } + EnableDnsHostnames = true + EnableDnsSupport = true + Tags = [ + { + Key = "Name" + Value = { "Fn::Sub" = "$${ProjectName}-$${Environment}-vpc" } + } + ] + } + } + + InternetGateway = { + Type = "AWS::EC2::InternetGateway" + Properties = { + Tags = [ + { + Key = "Name" + Value = { "Fn::Sub" = "$${ProjectName}-$${Environment}-igw" } + } + ] + } + } + + AttachGateway = { + Type = "AWS::EC2::VPCGatewayAttachment" + Properties = { + VpcId = { Ref = "VPC" } + InternetGatewayId = { Ref = "InternetGateway" } + } + } + + PublicSubnet = { + Type = "AWS::EC2::Subnet" + Properties = { + VpcId = { Ref = "VPC" } + CidrBlock = "10.0.1.0/24" + AvailabilityZone = { "Fn::Select" = [0, { "Fn::GetAZs" = "" }] } + MapPublicIpOnLaunch = true + Tags = [ + { + Key = "Name" + Value = { "Fn::Sub" = "$${ProjectName}-$${Environment}-public-subnet" } + } + ] + } + } + } + + Outputs = { + VPCId = { + Description = "VPC ID" + Value = { Ref = "VPC" } + Export = { + Name = { "Fn::Sub" = "$${ProjectName}-$${Environment}-VPC-ID" } + } + } + PublicSubnetId = { + Description = "Public Subnet ID" + Value = { Ref = "PublicSubnet" } + Export = { + Name = { "Fn::Sub" = "$${ProjectName}-$${Environment}-PublicSubnet-ID" } + } + } + } + }) + + capabilities = ["CAPABILITY_IAM"] + + tags = { + Name = "${var.project_name}-${var.environment}-network" + ManagedBy = "OpenTofu" + } +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..fb9dd8c --- /dev/null +++ b/outputs.tf @@ -0,0 +1,113 @@ +# CloudFront Distribution Outputs +output "cloudfront_distribution_id" { + description = "CloudFront distribution ID" + value = aws_cloudfront_distribution.main.id +} + +output "cloudfront_distribution_arn" { + description = "CloudFront distribution ARN" + value = aws_cloudfront_distribution.main.arn +} + +output "cloudfront_domain_name" { + description = "CloudFront distribution domain name" + value = aws_cloudfront_distribution.main.domain_name +} + +output "cloudfront_hosted_zone_id" { + description = "CloudFront distribution hosted zone ID" + value = aws_cloudfront_distribution.main.hosted_zone_id +} + +output "cloudfront_status" { + description = "CloudFront distribution status" + value = aws_cloudfront_distribution.main.status +} + +# CloudFormation Stack Outputs (conditional) +output "cloudformation_stack_id" { + description = "CloudFormation stack ID" + value = var.enable_cloudformation_stack ? aws_cloudformation_stack.network[0].id : null +} + +output "cloudformation_stack_name" { + description = "CloudFormation stack name" + value = var.enable_cloudformation_stack ? aws_cloudformation_stack.network[0].name : null +} + +output "vpc_id" { + description = "VPC ID from CloudFormation stack" + value = var.enable_cloudformation_stack ? data.aws_cloudformation_stack.network[0].outputs["VPCId"] : null +} + +output "public_subnet_id" { + description = "Public subnet ID from CloudFormation stack" + value = var.enable_cloudformation_stack ? data.aws_cloudformation_stack.network[0].outputs["PublicSubnetId"] : null +} + +# Security Group Outputs (conditional) +output "alb_security_group_id" { + description = "ALB security group ID" + value = var.create_alb_security_group ? aws_security_group.alb[0].id : null +} + +output "web_security_group_id" { + description = "Web server security group ID" + value = var.create_web_security_group ? aws_security_group.web[0].id : null +} + +# WAF Outputs (conditional) +output "waf_web_acl_arn" { + description = "WAF Web ACL ARN" + value = var.enable_waf ? aws_wafv2_web_acl.cloudfront[0].arn : null +} + +output "waf_web_acl_id" { + description = "WAF Web ACL ID" + value = var.enable_waf ? aws_wafv2_web_acl.cloudfront[0].id : null +} + +# Origin Information +output "origin_domain" { + description = "Origin domain name" + value = var.origin_domain +} + +# ACM Certificate Outputs +output "acm_certificate_arn" { + description = "ACM certificate ARN" + value = var.create_acm_certificate ? aws_acm_certificate.main[0].arn : null +} + +output "acm_certificate_domain_validation_options" { + description = "ACM certificate domain validation options" + value = var.create_acm_certificate ? aws_acm_certificate.main[0].domain_validation_options : null +} + +# Route53 Outputs +output "route53_zone_id" { + description = "Route53 hosted zone ID" + value = var.create_route53_records ? data.aws_route53_zone.main[0].zone_id : null +} + +# CloudFront URLs for testing +output "cloudfront_url" { + description = "CloudFront distribution URL" + value = "https://${aws_cloudfront_distribution.main.domain_name}" +} + +output "custom_domain_urls" { + description = "Custom domain URLs" + value = [for alias in var.cloudfront_aliases : "https://${alias}"] +} + +output "domain_validation_records" { + description = "DNS records needed for domain validation (if not using Route53)" + value = var.create_route53_records || !var.create_acm_certificate ? null : [ + for dvo in aws_acm_certificate.main[0].domain_validation_options : { + name = dvo.resource_record_name + type = dvo.resource_record_type + value = dvo.resource_record_value + } + ] +} \ No newline at end of file diff --git a/security.tf b/security.tf new file mode 100644 index 0000000..0a3bba0 --- /dev/null +++ b/security.tf @@ -0,0 +1,181 @@ +# Security Group for ALB/ELB (if needed for origin server) +resource "aws_security_group" "alb" { + count = var.create_alb_security_group ? 1 : 0 + name = "${var.project_name}-${var.environment}-alb-sg" + description = "Security group for ALB" + vpc_id = var.enable_cloudformation_stack ? data.aws_cloudformation_stack.network[0].outputs["VPCId"] : null + + # HTTP access from anywhere + ingress { + description = "HTTP" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # HTTPS access from anywhere + ingress { + description = "HTTPS" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + # All outbound traffic + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-${var.environment}-alb-sg" + } +} + +# Security Group for EC2 instances (if needed) +resource "aws_security_group" "web" { + count = var.create_web_security_group ? 1 : 0 + name = "${var.project_name}-${var.environment}-web-sg" + description = "Security group for web servers" + vpc_id = var.enable_cloudformation_stack ? data.aws_cloudformation_stack.network[0].outputs["VPCId"] : null + + # HTTP access from ALB security group + ingress { + description = "HTTP from ALB" + from_port = 80 + to_port = 80 + protocol = "tcp" + security_groups = var.create_alb_security_group ? [aws_security_group.alb[0].id] : [] + } + + # HTTPS access from ALB security group + ingress { + description = "HTTPS from ALB" + from_port = 443 + to_port = 443 + protocol = "tcp" + security_groups = var.create_alb_security_group ? [aws_security_group.alb[0].id] : [] + } + + # SSH access (restrict as needed) + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = var.ssh_allowed_cidrs + } + + # All outbound traffic + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-${var.environment}-web-sg" + } +} + +# WAF Web ACL for CloudFront (optional) +resource "aws_wafv2_web_acl" "cloudfront" { + provider = aws.us_east_1 # CloudFront WAF must be in us-east-1 + count = var.enable_waf ? 1 : 0 + name = "${var.project_name}-${var.environment}-waf" + scope = "CLOUDFRONT" + + default_action { + allow {} + } + + # Rate limiting rule + rule { + name = "RateLimitRule" + priority = 1 + + action { + block {} + } + + statement { + rate_based_statement { + limit = 10000 + aggregate_key_type = "IP" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "RateLimitRule" + sampled_requests_enabled = true + } + } + + # AWS Managed Rules - Core Rule Set + rule { + name = "AWSManagedRulesCommonRuleSet" + priority = 2 + + override_action { + none {} + } + + statement { + managed_rule_group_statement { + name = "AWSManagedRulesCommonRuleSet" + vendor_name = "AWS" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "CommonRuleSetMetric" + sampled_requests_enabled = true + } + } + + # AWS Managed Rules - Known Bad Inputs + rule { + name = "AWSManagedRulesKnownBadInputsRuleSet" + priority = 3 + + override_action { + none {} + } + + statement { + managed_rule_group_statement { + name = "AWSManagedRulesKnownBadInputsRuleSet" + vendor_name = "AWS" + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "KnownBadInputsRuleSetMetric" + sampled_requests_enabled = true + } + } + + tags = { + Name = "${var.project_name}-${var.environment}-waf" + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${var.project_name}-${var.environment}-waf" + sampled_requests_enabled = true + } +} + +# Data source to get CloudFormation stack outputs (conditional) +data "aws_cloudformation_stack" "network" { + count = var.enable_cloudformation_stack ? 1 : 0 + name = aws_cloudformation_stack.network[0].name +} \ No newline at end of file diff --git a/setup-backend.sh b/setup-backend.sh new file mode 100755 index 0000000..aa202cc --- /dev/null +++ b/setup-backend.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# Setup script for S3 backend and CloudFront logging +# This creates the necessary S3 buckets and DynamoDB table + +AWS_REGION="us-east-1" +AWS_ACCOUNT_ID="535294143817" +STATE_BUCKET="aws-cf-terraform-state-${AWS_ACCOUNT_ID}" +LOGS_BUCKET="aws-cf-cloudfront-logs-${AWS_ACCOUNT_ID}" +DYNAMODB_TABLE="terraform-state-lock" + +echo "Setting up S3 backend and CloudFront logging infrastructure..." + +# Create S3 bucket for Terraform state +echo "Creating S3 bucket for Terraform state: ${STATE_BUCKET}" +aws s3api create-bucket \ + --bucket ${STATE_BUCKET} \ + --region ${AWS_REGION} \ + 2>/dev/null || echo "State bucket already exists or error occurred" + +# Enable versioning on state bucket +echo "Enabling versioning on state bucket..." +aws s3api put-bucket-versioning \ + --bucket ${STATE_BUCKET} \ + --versioning-configuration Status=Enabled + +# Enable encryption on state bucket +echo "Enabling encryption on state bucket..." +aws s3api put-bucket-encryption \ + --bucket ${STATE_BUCKET} \ + --server-side-encryption-configuration '{ + "Rules": [ + { + "ApplyServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }' + +# Block public access on state bucket +echo "Blocking public access on state bucket..." +aws s3api put-public-access-block \ + --bucket ${STATE_BUCKET} \ + --public-access-block-configuration \ + "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" + +# Create S3 bucket for CloudFront logs +echo "Creating S3 bucket for CloudFront logs: ${LOGS_BUCKET}" +aws s3api create-bucket \ + --bucket ${LOGS_BUCKET} \ + --region ${AWS_REGION} \ + 2>/dev/null || echo "Logs bucket already exists or error occurred" + +# Set bucket ACL for CloudFront logging +echo "Setting ACL for CloudFront logs bucket..." +aws s3api put-bucket-acl \ + --bucket ${LOGS_BUCKET} \ + --grant-write 'URI="http://acs.amazonaws.com/groups/s3/LogDelivery"' \ + --grant-read-acp 'URI="http://acs.amazonaws.com/groups/s3/LogDelivery"' + +# Add lifecycle policy to logs bucket (optional - delete old logs after 90 days) +echo "Adding lifecycle policy to logs bucket..." +aws s3api put-bucket-lifecycle-configuration \ + --bucket ${LOGS_BUCKET} \ + --lifecycle-configuration '{ + "Rules": [ + { + "Id": "DeleteOldLogs", + "Status": "Enabled", + "Expiration": { + "Days": 90 + }, + "NoncurrentVersionExpiration": { + "NoncurrentDays": 30 + } + } + ] + }' + +# Create DynamoDB table for state locking +echo "Creating DynamoDB table for state locking: ${DYNAMODB_TABLE}" +aws dynamodb create-table \ + --table-name ${DYNAMODB_TABLE} \ + --attribute-definitions AttributeName=LockID,AttributeType=S \ + --key-schema AttributeName=LockID,KeyType=HASH \ + --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \ + --region ${AWS_REGION} \ + 2>/dev/null || echo "DynamoDB table already exists or error occurred" + +echo "" +echo "βœ… Backend setup complete!" +echo "" +echo "πŸ“¦ S3 State Bucket: ${STATE_BUCKET}" +echo "πŸ“Š S3 Logs Bucket: ${LOGS_BUCKET}" +echo "πŸ”’ DynamoDB Table: ${DYNAMODB_TABLE}" +echo "" +echo "Next steps:" +echo "1. Run: tofu init -migrate-state" +echo "2. Run: tofu apply to update CloudFront with logging" \ No newline at end of file diff --git a/terraform.tfvars.example b/terraform.tfvars.example new file mode 100644 index 0000000..3ea0c2b --- /dev/null +++ b/terraform.tfvars.example @@ -0,0 +1,39 @@ +# AWS Configuration +aws_region = "us-east-1" +project_name = "aws-cf" +environment = "dev" + +# Origin Configuration +origin_domain = "origin.servidor.it.com" + +# Domain Configuration +domain_name = "servidor.it.com" + +# CloudFront Configuration +cloudfront_aliases = [ + "servidor.it.com", + "www.servidor.it.com" +] + +# Route53 Configuration +create_route53_records = true +certificate_domain_validation_options = "DNS" + +# CloudFront Settings +price_class = "PriceClass_100" # PriceClass_All, PriceClass_200, PriceClass_100 +origin_protocol_policy = "https-only" # http-only, https-only, match-viewer +viewer_protocol_policy = "redirect-to-https" # allow-all, https-only, redirect-to-https + +# Cache Policies (AWS Managed Policies) +cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # CachingDisabled +origin_request_policy_id = "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf" # CORS-S3Origin + +# Security Configuration +create_alb_security_group = false +create_web_security_group = false +enable_waf = false + +# SSH Access (if creating EC2 security groups) +ssh_allowed_cidrs = [ + "0.0.0.0/0" # Restrict this to your IP in production +] \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..ed38111 --- /dev/null +++ b/variables.tf @@ -0,0 +1,159 @@ +variable "aws_region" { + description = "AWS region" + type = string + default = "us-east-1" +} + +variable "project_name" { + description = "Name of the project" + type = string + default = "aws-cf" +} + +variable "environment" { + description = "Environment (dev, staging, prod)" + type = string + default = "dev" +} + +variable "origin_domain" { + description = "Origin domain name" + type = string + default = "origin.servidor.it.com" +} + +variable "cloudfront_aliases" { + description = "List of aliases for CloudFront distribution" + type = list(string) + default = ["servidor.it.com", "www.servidor.it.com"] +} + +variable "domain_name" { + description = "Main domain name for ACM certificate" + type = string + default = "servidor.it.com" +} + +variable "certificate_domain_validation_options" { + description = "Domain validation method for ACM certificate" + type = string + default = "DNS" +} + +variable "create_route53_records" { + description = "Whether to create Route53 records for domain validation and alias" + type = bool + default = true +} + +variable "price_class" { + description = "CloudFront distribution price class" + type = string + default = "PriceClass_All" + + validation { + condition = contains([ + "PriceClass_All", + "PriceClass_200", + "PriceClass_100" + ], var.price_class) + error_message = "Price class must be one of: PriceClass_All, PriceClass_200, PriceClass_100" + } +} + +variable "origin_protocol_policy" { + description = "Origin protocol policy" + type = string + default = "https-only" + + validation { + condition = contains([ + "http-only", + "https-only", + "match-viewer" + ], var.origin_protocol_policy) + error_message = "Origin protocol policy must be one of: http-only, https-only, match-viewer" + } +} + +variable "viewer_protocol_policy" { + description = "Viewer protocol policy" + type = string + default = "redirect-to-https" + + validation { + condition = contains([ + "allow-all", + "https-only", + "redirect-to-https" + ], var.viewer_protocol_policy) + error_message = "Viewer protocol policy must be one of: allow-all, https-only, redirect-to-https" + } +} + +variable "cache_policy_id" { + description = "CloudFront cache policy ID (managed or custom)" + type = string + default = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # CachingDisabled +} + +variable "origin_request_policy_id" { + description = "CloudFront origin request policy ID" + type = string + default = "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf" # CORS-S3Origin +} + +# Security-related variables +variable "create_alb_security_group" { + description = "Whether to create ALB security group" + type = bool + default = false +} + +variable "create_web_security_group" { + description = "Whether to create web server security group" + type = bool + default = false +} + +variable "ssh_allowed_cidrs" { + description = "CIDR blocks allowed for SSH access" + type = list(string) + default = ["0.0.0.0/0"] # Restrict this in production +} + +variable "enable_waf" { + description = "Whether to enable WAF for CloudFront" + type = bool + default = false +} + +variable "enable_cloudformation_stack" { + description = "Whether to create CloudFormation stack" + type = bool + default = false +} + +variable "create_acm_certificate" { + description = "Whether to create ACM certificate" + type = bool + default = true +} + +variable "enable_cloudfront_logging" { + description = "Whether to enable CloudFront access logging" + type = bool + default = true +} + +variable "cloudfront_logs_bucket" { + description = "S3 bucket for CloudFront logs" + type = string + default = "" +} + +variable "cloudfront_logs_prefix" { + description = "Prefix for CloudFront logs in S3" + type = string + default = "cloudfront-logs/" +} \ No newline at end of file diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..31ece35 --- /dev/null +++ b/versions.tf @@ -0,0 +1,43 @@ +terraform { + required_version = ">= 1.5" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + # Uncomment and configure if you want to use remote state + # backend "s3" { + # bucket = "your-tofu-state-bucket" + # key = "cloudfront/terraform.tfstate" + # region = "us-east-1" + # } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "OpenTofu" + } + } +} + +# Additional provider for ACM certificate (must be in us-east-1 for CloudFront) +provider "aws" { + alias = "us_east_1" + region = "us-east-1" + + default_tags { + tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "OpenTofu" + } + } +} \ No newline at end of file