From ef9f091777c4d09c364145d904efc748684b0357 Mon Sep 17 00:00:00 2001 From: Alper Date: Fri, 5 Jun 2026 15:46:02 +0300 Subject: [PATCH] cicd: creates s3 deploy automation --- .github/workflows/deploy.yml | 40 ++++++++++++++++++ infra/README.md | 29 +++++++++++-- infra/github_oidc.tf | 74 ++++++++++++++++++++++++++++++++++ infra/outputs.tf | 5 +++ infra/terraform.tfvars.example | 1 + infra/variables.tf | 6 +++ 6 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 infra/github_oidc.tf diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..2bf3966 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,40 @@ +name: Deploy static site + +on: + push: + branches: + - main + +permissions: + contents: read + id-token: write + +jobs: + deploy: + name: Build and deploy to S3 + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build static site + run: npm run build + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v5 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Sync static site to S3 + run: aws s3 sync out "s3://${{ secrets.S3_BUCKET_NAME }}" --delete diff --git a/infra/README.md b/infra/README.md index fca22e2..53fa184 100644 --- a/infra/README.md +++ b/infra/README.md @@ -10,7 +10,6 @@ This folder creates the S3 bucket that will store the exported Next.js files fro - Region: `eu-central-1` - Bucket name: generated from `var.bucket_name_prefix` plus a Terraform-managed random suffix -- Current bucket name: `alper-dev-cc4edc59` - Access strategy: private S3 bucket with all public access blocked - CloudFront strategy: use the bucket regional domain name as the CloudFront origin and grant access later with CloudFront Origin Access Control (OAC) @@ -19,9 +18,9 @@ The bucket is not configured as a public S3 website endpoint. Static files shoul ## CloudFront Configuration - Public entry point: CloudFront distribution -- Distribution ID: `E2M2CTUFRRX4GS` -- Distribution domain: `d1fmmfkzwx4n74.cloudfront.net` -- Distribution URL: `https://d1fmmfkzwx4n74.cloudfront.net` +- Distribution ID: available from `terraform output cloudfront_distribution_id` +- Distribution domain: available from `terraform output cloudfront_domain_name` +- Distribution URL: available from `terraform output cloudfront_url` - Origin: private S3 bucket regional domain name - S3 access: CloudFront Origin Access Control (OAC) - Default root object: `index.html` @@ -56,3 +55,25 @@ Useful output for the future CloudFront task: ## Static Hosting Notes Upload the contents of the Next.js `out/` directory to the bucket after running `npm run build`. The bucket should remain private; public traffic should go through CloudFront. + +## Deployment Workflow + +The GitHub Actions deployment workflow builds the static site and syncs `out/` to S3 on pushes to `main`. + +Required GitHub secrets: + +- `AWS_ROLE_TO_ASSUME`: IAM role ARN from `terraform output github_actions_deploy_role_arn`. +- `AWS_REGION`: AWS region for deployment. +- `S3_BUCKET_NAME`: target bucket name from `terraform output bucket_name`. + +The workflow uses `aws s3 sync out s3://$S3_BUCKET_NAME --delete` so removed files are deleted from S3 during deployment. CloudFront invalidation is intentionally out of scope. + +## GitHub OIDC + +Terraform creates an IAM OIDC provider for `token.actions.githubusercontent.com` and a deployment role that can only be assumed by this repository's `main` branch: + +- Repository: `replakcan/alper-dev` +- Branch: `main` +- Role output: `github_actions_deploy_role_arn` + +The role allows only the S3 permissions needed by the deployment workflow: list the static site bucket and get, put, or delete objects in it. It does not grant CloudFront invalidation permissions. diff --git a/infra/github_oidc.tf b/infra/github_oidc.tf new file mode 100644 index 0000000..b678821 --- /dev/null +++ b/infra/github_oidc.tf @@ -0,0 +1,74 @@ +data "aws_iam_policy_document" "github_actions_assume_role" { + statement { + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.github.arn] + } + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:sub" + values = ["repo:${var.github_repository}:ref:refs/heads/main"] + } + } +} + +data "aws_iam_policy_document" "github_actions_deploy" { + statement { + sid = "ListStaticSiteBucket" + + actions = ["s3:ListBucket"] + + resources = [ + aws_s3_bucket.site.arn, + ] + } + + statement { + sid = "SyncStaticSiteObjects" + + actions = [ + "s3:DeleteObject", + "s3:GetObject", + "s3:PutObject", + ] + + resources = [ + "${aws_s3_bucket.site.arn}/*", + ] + } +} + +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + + client_id_list = [ + "sts.amazonaws.com", + ] +} + +resource "aws_iam_role" "github_actions_deploy" { + name = "${var.project_name}-${var.environment}-github-actions-deploy" + assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json + + tags = { + Name = "${var.project_name}-${var.environment}-github-actions-deploy" + Environment = var.environment + Project = var.project_name + ManagedBy = "Terraform" + } +} + +resource "aws_iam_role_policy" "github_actions_deploy" { + name = "${var.project_name}-${var.environment}-s3-deploy" + role = aws_iam_role.github_actions_deploy.id + policy = data.aws_iam_policy_document.github_actions_deploy.json +} diff --git a/infra/outputs.tf b/infra/outputs.tf index 6359deb..64d18aa 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -42,3 +42,8 @@ output "cloudfront_url" { description = "CloudFront URL for the static website." value = "https://${aws_cloudfront_distribution.site.domain_name}" } + +output "github_actions_deploy_role_arn" { + description = "IAM role ARN that GitHub Actions should assume through OIDC for S3 deployment." + value = aws_iam_role.github_actions_deploy.arn +} diff --git a/infra/terraform.tfvars.example b/infra/terraform.tfvars.example index 9c65401..65ca442 100644 --- a/infra/terraform.tfvars.example +++ b/infra/terraform.tfvars.example @@ -2,3 +2,4 @@ aws_region = "eu-central-1" bucket_name_prefix = "alper-dev" environment = "dev" project_name = "alper-dev" +github_repository = "replakcan/alper-dev" diff --git a/infra/variables.tf b/infra/variables.tf index 0af2c95..8d31983 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -26,3 +26,9 @@ variable "project_name" { type = string default = "alper-dev" } + +variable "github_repository" { + description = "GitHub repository allowed to assume the deployment role through OIDC." + type = string + default = "replakcan/alper-dev" +}