Home How To Deploy an S3 Site with Github Actions
Post
Cancel

How To Deploy an S3 Site with Github Actions

If you haven’t read the Landing Page Project Kickoff or the How To Setup Route53 with Terraform post, this post builds upon both of those. At a minimum, this guide requires a public hosted zone created in Route53.

  1. Setup the repo
  2. Setup the infrastructure
  3. Setup the pipeline

Static Site

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
module "s3_bucket" {
  source = "terraform-aws-modules/s3-bucket/aws"

  bucket = "terraform-aws-modules-example.com"

  force_destroy       = true

  acl = "private" # "acl" conflicts with "grant" and "owner"

  # S3 bucket-level Public Access Block configuration
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true

  website = {

    index_document = "index.html"
    error_document = "error.html"
  }

  # Bucket policies
  attach_policy                         = true
  policy                                = data.aws_iam_policy_document.bucket_policy.json
  attach_deny_insecure_transport_policy = true
  attach_require_latest_tls_policy      = true

}

data "aws_iam_policy_document" "bucket_policy" {
  statement {

    sid = "CloudfrontReadObject"

    principals {
      type        = "AWS"
      identifiers = module.cdn.cloudfront_origin_access_identity_iam_arns
    }

    effect = "Allow"

    actions = [
      "s3:GetObject",
    ]

    resources = [
      module.s3_bucket.s3_bucket_arn,
      "${module.s3_bucket.s3_bucket_arn}/*",
    ]
  }
}

module "cdn" {
  source = "terraform-aws-modules/cloudfront/aws"

  aliases = ["www.terraform-aws-modules-example.com", "terraform-aws-modules-example.com"]

  comment             = "My awesome CloudFront"
  enabled             = true
  is_ipv6_enabled     = true
  price_class         = "PriceClass_All"
  retain_on_delete    = false
  wait_for_deployment = false
  default_root_object = "index.html" # Super important - https://serverfault.com/questions/581268/amazon-cloudfront-with-s3-access-denied

  create_origin_access_identity = true
  origin_access_identities = {
    s3_bucket_one = "My awesome CloudFront can access"
  }

  origin = {
    s3_one = {
      domain_name = module.s3_bucket.s3_bucket_bucket_regional_domain_name
      s3_origin_config = {
        origin_access_identity = "s3_bucket_one" # key in `origin_access_identities`
      }
    }
  }

  default_cache_behavior = {
    target_origin_id           = "s3_one"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods = ["GET", "HEAD", "OPTIONS"]
    cached_methods  = ["GET", "HEAD"]
    compress        = true
    query_string    = true
  }

  viewer_certificate = {
    acm_certificate_arn = module.acm.acm_certificate_arn
    ssl_support_method  = "sni-only"
  }
}

module "acm" {
  source  = "terraform-aws-modules/acm/aws"
  version = "~> 4.0"

  domain_name  = "terraform-aws-modules-example.com"
  zone_id      = "Z123456789YV12345M1SQ"

  subject_alternative_names = [
    "*.terraform-aws-modules-example.com",
  ]

  wait_for_validation = true
  create_route53_records = true

  tags = {
    Name = "terraform-aws-modules-example.com"
  }
}

##########
# Route53
##########

module "records" {
  source  = "terraform-aws-modules/route53/aws//modules/records"
  version = "~> 2.0"

  zone_id = "Z123456789YV12345M1SQ"

  records = [
    {
      name = ""
      type = "A"
      alias = {
        name    = module.cdn.cloudfront_distribution_domain_name
        zone_id = module.cdn.cloudfront_distribution_hosted_zone_id
      }
    },
    {
      name = "www"
      type = "A"
      alias = {
        name    = module.cdn.cloudfront_distribution_domain_name
        zone_id = module.cdn.cloudfront_distribution_hosted_zone_id
      }
    },
  ]
}

Deployment Role

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
resource "aws_iam_openid_connect_provider" "github_actions" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

module "github_actions_s3_deploy_role" {
  source = "terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc"

  create_role = true

  role_name = "github-actions-s3-deploy-role"

  provider_url = aws_iam_openid_connect_provider.github_actions.url

  role_policy_arns = [
    aws_iam_policy.github_actions_s3_deploy.arn
  ]

  number_of_role_policy_arns = 1
}


resource "aws_iam_policy" "github_actions_s3_deploy" {
  name = "github-actions-s3-deploy"
  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Sid" : "AllowSync",
        "Effect" : "Allow",
        "Action" : [
          "s3:GetObject",
          "s3:Listbucket",
          "s3:PutObject"
        ],
        "Resource" : "*"
      }
    ]
  })
}

Github Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: Upload Website

on:
 push:
   branches:
     - main
     - master

permissions:
  id-token: write
  contents: read

jobs:
 Deploy:
   runs-on: ubuntu-latest
   steps:
     - name: Checkout
       uses: actions/checkout@v2

     - name: Configure AWS credentials from Test account
       uses: aws-actions/configure-aws-credentials@v1
       with:
         role-to-assume: ${{ secrets.AWS_ROLE }}
         aws-region: ${{ secrets.AWS_REGION }}

     - name: Deploy static site to S3 bucket
       run: aws s3 sync ./src s3://${{ secrets.BUCKET_NAME }}

This post is licensed under CC BY 4.0 by the author.