Ash-Docs
Pro-Tier DevOps

Automated Cloud Hosting Using AWS, Terraform and GitHub Actions

Stop manually uploading files to S3. In this lab, we build an Automated Pipeline. Terraform architects a secure "Private" bucket fronted by CloudFront for global speed, while GitHub Actions teleports your code the moment you push it. This is a total Win for your automated workflows.

Architecture Overview

Architecture Flow: Terraform, GitHub Actions, S3, CloudFront OAC

The Architecture (The Flow)

Understanding the data flow is key to mastering DevOps. This architecture ensures your code moves securely from your local machine to a global audience without any manual intervention.

Step 1: Code Push

The journey begins when you push your code updates to the GitHub main branch.

Step 2: Automation (GitHub Actions)

GitHub Actions detects the push, assumes your IAM credentials, and automatically syncs the new files to S3.

Step 3: Secure Storage (S3)

Your files are stored in Amazon S3. Public access is disabled, keeping your origin safe from direct internet exposure.

Step 4: Distribution (CloudFront)

CloudFront acts as the "Security Guard," pulling files from S3 and serving them globally via HTTPS.

Step 5: DNS Management (Route 53)

Route 53 points your professional custom domain (thanvirassif.com) directly to the CloudFront network.


0 Required Prerequisites

Terraform

Infrastructure as Code

GitHub Repo

For Your Website Code

AWS Account

With Route 53 Zone

IAM Access

Admin Permissions

1 Infrastructure Blueprint (`main.tf`)

This builds the S3 Bucket (Private), CloudFront with OAC, and Route 53 Records. No manual clicks required!

Terraform Core Infrastructure (main.tf)
# ---Configure the AWS provider---
provider "aws" {
  region = "ap-south-1" 
}

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}


# ---DNS and SSL (Route 53 & ACM)---
data "aws_route53_zone" "main" {
  name = "youdomain.com"
}

resource "aws_acm_certificate" "cert" {
  provider          = aws.us_east_1
  domain_name       = "www.youdomain.com"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.main.zone_id
}

# This tells Terraform: "Wait until the certificate is actually issued before moving on"
resource "aws_acm_certificate_validation" "cert" {
  provider                = aws.us_east_1
  certificate_arn         = aws_acm_certificate.cert.arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}


# ---S3 Bucket for Static Website Hosting---
resource "aws_s3_bucket" "website_bucket" {
  bucket = "www.yourdomain.com"
}

resource "aws_s3_bucket_public_access_block" "block_public" {
  bucket = aws_s3_bucket.website_bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}


# ---CloudFront with OAC (Origin Access Control)---
# Create the OAC
resource "aws_cloudfront_origin_access_control" "oac" {
  name                              = "s3-oac-test-site"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "distro" {
  origin {
    domain_name              = aws_s3_bucket.website_bucket.bucket_regional_domain_name
    origin_id                = "S3Origin"
    origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
  }

  enabled             = true
  default_root_object = "index.html"
  aliases             = ["www.yourdomain.com"]

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "S3Origin"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate_validation.cert.certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }
}


# ---(Final) IAM Role and Policy for CloudFront OAC---
# Allow CloudFront to access S3
resource "aws_s3_bucket_policy" "allow_cloudfront" {
  bucket = aws_s3_bucket.website_bucket.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "AllowCloudFrontServicePrincipalReadOnly"
        Effect    = "Allow"
        Principal = { Service = "cloudfront.amazonaws.com" }
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.website_bucket.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.distro.arn
          }
        }
      }
    ]
  })
}

# Route 53 Alias Record
resource "aws_route53_record" "www" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = "www.yourdomain.com"
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.distro.domain_name
    zone_id                = aws_cloudfront_distribution.distro.hosted_zone_id
    evaluate_target_health = false
  }
}

output "s3_bucket_name" {
  value = aws_s3_bucket.website_bucket.id
}

output "cloudfront_distribution_id" {
  value = aws_cloudfront_distribution.distro.id
}

output "website_url" {
  value = "https://www.yourdomain.com"
}

2 IAM Least Privilege (`iam.tf`)

Security first. We create a dedicated user for GitHub that can only touch your specific bucket and clear the cache. No cap, this is how pros do it.

Permissions Script (iam.tf)
# --- 1. AWS Provider Configuration --- Provider is already defined in main.tf, so we don't need to repeat it here. Just make sure to run `terraform init` in the root directory to set everything up before applying this IAM configuration.
# --- 2. Create the IAM User ---
# This is the "Identity" that GitHub Actions will use to log in.
resource "aws_iam_user" "github_actions" {
  name = "github-actions-auto-cloud-deploy"
  tags = {
    Project = "AutoCloudDeploy"
    Owner   = "yourname"
  }
}


# --- 3. Define the Least Privilege Policy ---
# We are only giving GitHub permission to touch what it absolutely needs.
resource "aws_iam_policy" "deploy_policy" {
  name        = "GitHubActionsDeployPolicy"
  description = "Allows S3 sync and CloudFront invalidation for the test subdomain"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        # Permission: Manage files in the specific S3 bucket
        Effect = "Allow"
        Action = [
          "s3:PutObject",
          "s3:ListBucket",
          "s3:DeleteObject",
          "s3:GetBucketLocation"
        ]
        Resource = [
          "${aws_s3_bucket.website_bucket.arn}",
          "${aws_s3_bucket.website_bucket.arn}/*"
        ]
      },
      {
        # Permission: Tell CloudFront to refresh its cache
        Effect = "Allow"
        Action = [
          "cloudfront:CreateInvalidation"
        ]
        Resource = [
          "${aws_cloudfront_distribution.distro.arn}"
        ]
      }
    ]
  })
}


# --- 4. Attach Policy to User ---
# This links the "locked door" (the policy) to the "key holder" (the user).
resource "aws_iam_user_policy_attachment" "attach_deploy" {
  user       = aws_iam_user.github_actions.name
  policy_arn = aws_iam_policy.deploy_policy.arn
}


# --- 5. Create Access Keys ---
# This generates the credentials you will paste into GitHub Secrets.
resource "aws_iam_access_key" "github_keys" {
  user = aws_iam_user.github_actions.name
}


# --- 6. Outputs (The "The Goods") ---
# These are sensitive. Terraform won't show them unless you specifically ask.
output "github_actions_access_key" {
  value     = aws_iam_access_key.github_keys.id
  sensitive = true
}

output "github_actions_secret_key" {
  value     = aws_iam_access_key.github_keys.secret
  sensitive = true
}

3 Critical: Link GitHub to AWS

Run terraform apply first, then grab your secrets from the terminal. Go to your GitHub Repo -> Settings -> Secrets -> Actions and add these:

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • DIST_ID (CloudFront ID)

4 The Robot (`deploy.yml`)

Save this in .github/workflows/. It wakes up on every push to main and syncs your files.

GitHub Actions Workflow
name: Auto Cloud Deploy

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-south-1

      - name: Sync to S3
        run: aws s3 sync . s3://your-bucket-name --delete --exclude ".git/*" --exclude "*.tf"

      - name: Refresh Cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.DIST_ID }} --paths "/*"

The Terraform Command Launch

Now that your main.tf and iam.tf are ready, it's time to talk to the AWS cloud. Open your terminal in the project folder and follow these exact steps to deploy the "SkySync Pro" engine.

1. Initialize Terraform

This command downloads the AWS providers and prepares your working directory.

Terminal
terraform init

2. Verify the Plan

Always check what you're building. This shows you the 12+ resources being created.

Terminal
terraform plan

3. Execute & Build

The moment of truth. Type yes when prompted to start the build.

Terminal
terraform apply

4. Export Secret Keys

Since your Secret Key is marked as sensitive, use this command to view it for GitHub Secrets.

Terminal
terraform output -raw secret_key

Critical Notes & Security Checklist

Note 1: IAM Least Privilege (MNC Standard)

Never use your "Root" AWS credentials in GitHub. Always use a dedicated IAM user with only the specific permissions defined in our iam.tf. Following the Principle of Least Privilege is a non-negotiable requirement for Indian MNCs like Amazon and Other reputable companies.

Note 2: SSL Certification

CloudFront is picky-it requires all SSL certificates to be created in the us-east-1 (N. Virginia) region. Our Terraform code handles this automatically using the aws.us_east_1 provider alias.

Note 3: Private vs Public

Your S3 bucket is 100% Private. Secure access is granted only to CloudFront via the Origin Access Control (OAC) policy. Do not change the S3 public access settings or you'll break the security chain!

Note 4: The Cleanup (Deletion)

Since we added force_destroy = true in the main.tf, Terraform will automatically empty the bucket before deleting it. This ensures you aren't charged for resources after you finish testing.

terraform destroy

5 Cleanup Protocol

Avoid the Surprise Bill

Once your students are done testing, run terraform destroy. Because we added force_destroy = true, Terraform will wipe the bucket and all files inside automatically.

🎉 Pipeline Fully Operational!

You've successfully bridged GitHub, Terraform, and AWS into a seamless flow. Big flex!

Check your website out using your domain name.

Thanvir Assif

Created by Thanvir Assif

Cloud & DevOps Engineer | AWS Certified | Full Stack Developer. Helping professionals and enthusiasts master cloud deployments.

© 2026 Thanvir Assif. All rights reserved.