|
| 1 | +# Deploy to an S3 bucket from GitHub |
| 2 | + |
| 3 | +This guide gives a quick, secure way to build a simple static website in GitHub, and deploy it to an S3 bucket in AWS. |
| 4 | + |
| 5 | +For that to work, GitHub needs permissions on the S3 bucket. The standard approach is to use a specific AWS access_key and secret_token. But, even if you use GitHub Secrets, you're still then exposing an access token to GitHub. |
| 6 | + |
| 7 | +OIDC is an alternative approach whereby GitHub gets granted temporary access to a specific role in AWS. This further lets you limit that role using IAM: to specific buckets and actions. |
| 8 | + |
| 9 | +The process flow for OIDC is: |
| 10 | +1. GitHub Action triggers from your git commit |
| 11 | +2. GitHub Action assumes a role in AWS |
| 12 | +3. (behind the scenes, GitHub and AWS use OIDC: exchanging JWT tokens to grant GitHub temporary access to a specific role in AWS) |
| 13 | +4. GitHub Action uses that role to copy files into an S3 bucket |
| 14 | + |
| 15 | +One-time setup to get this working: |
| 16 | +1. Define GitHub as an Identity Provider in your AWS account |
| 17 | +2. Define what GitHub is allowed to do (IAM policy) |
| 18 | +3. Define the GitHub role (IAM role) |
| 19 | +4. Define the GitHub Action |
| 20 | + |
| 21 | +NB: You should script as much of this as possible, where it is safe to do so. |
| 22 | + |
| 23 | +## Define GitHub as an identity provider in your AWS account |
| 24 | + |
| 25 | +This is done by adding GitHub as an IdP Provider in AWS. |
| 26 | + |
| 27 | +Follow steps to create IdP provider: |
| 28 | +https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services |
| 29 | + |
| 30 | + |
| 31 | +## Define what GitHub is allowed to do |
| 32 | + |
| 33 | +This is done by creating an AWS Role Profile, stating exactly which buckets GitHub will be allowed to deploy into. |
| 34 | + |
| 35 | +Create new Policy ("GitHubS3DeployPolicy") |
| 36 | + |
| 37 | +```json |
| 38 | +{ |
| 39 | + "Version": "2012-10-17", |
| 40 | + "Statement": [ |
| 41 | + { |
| 42 | + "Effect": "Allow", |
| 43 | + "Action": [ |
| 44 | + "s3:*", |
| 45 | + "s3-object-lambda:*" |
| 46 | + ], |
| 47 | + "Resource": [ |
| 48 | + "arn:aws:s3:::your-s3-bucket/*", |
| 49 | + "arn:aws:s3:::your-s3-bucket" |
| 50 | + ] |
| 51 | + } |
| 52 | + ] |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +## Define the GitHub role |
| 57 | + |
| 58 | +This is the role that GitHub will assume. It: |
| 59 | +- Links to the policy created early |
| 60 | +- Links to the github OIDC created early |
| 61 | +- Explicitly states which GitHub repo, and branches are permitted |
| 62 | + |
| 63 | +This page includes many options, including using Cognito, etc. |
| 64 | +https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html |
| 65 | + |
| 66 | +Below is a full, simple example that doesn't use Cognito. |
| 67 | + |
| 68 | +Create a new Role ("GitHubS3DeployRole") |
| 69 | +Trust policy: |
| 70 | + |
| 71 | +```json |
| 72 | +{ |
| 73 | + "Version": "2012-10-17", |
| 74 | + "Statement": [ |
| 75 | + { |
| 76 | + "Effect": "Allow", |
| 77 | + "Principal": { |
| 78 | + "Federated": "arn:aws:iam::<YOUR_AWS_ACCOUNT_NUMBER>:oidc-provider/token.actions.githubusercontent.com" |
| 79 | + }, |
| 80 | + "Action": "sts:AssumeRoleWithWebIdentity", |
| 81 | + "Condition": { |
| 82 | + "StringEquals": { |
| 83 | + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", |
| 84 | + "token.actions.githubusercontent.com:sub": "repo:<YOUR_GITHUB_ORG>/<YOUR_GITHUB_REPO>:ref:refs/heads/main" |
| 85 | + } |
| 86 | + } |
| 87 | + } |
| 88 | + ] |
| 89 | +} |
| 90 | +``` |
| 91 | +Attach the policy created earlier ("GitHubS3DeployPolicy") |
| 92 | + |
| 93 | + |
| 94 | +## Define the GitHub Action |
| 95 | + |
| 96 | +Define ASSUME_ROLE_ARN ("GitHubS3DeployRole" from earlier) and AWS_S3_BUCKET_NAME in GitHub Repo Secrets. |
| 97 | +Example below just syncs the "view-stack" folder into the s3 bucket. |
| 98 | + |
| 99 | +```yaml |
| 100 | +name: deploy-radar |
| 101 | + |
| 102 | +on: |
| 103 | + push: |
| 104 | + branches: |
| 105 | + - main |
| 106 | + |
| 107 | +jobs: |
| 108 | + build: |
| 109 | + permissions: |
| 110 | + id-token: write |
| 111 | + contents: read |
| 112 | + runs-on: ubuntu-latest |
| 113 | + timeout-minutes: 10 |
| 114 | + steps: |
| 115 | + - name: Checkout |
| 116 | + uses: actions/checkout@v3 |
| 117 | + |
| 118 | + - name: Authenticate with AWS over OIDC |
| 119 | + uses: aws-actions/configure-aws-credentials@v2 |
| 120 | + with: |
| 121 | + role-to-assume: ${{ secrets.ASSUME_ROLE_ARN }} |
| 122 | + role-session-name: mysessionname |
| 123 | + aws-region: eu-west-2 |
| 124 | + - name: Copy files to the s3 website content bucket |
| 125 | + run: |
| 126 | + aws s3 sync myfolder s3://${{ secrets.AWS_S3_BUCKET_NAME }}/myfolder |
| 127 | + aws s3 sync myotherfolder s3://${{ secrets.AWS_S3_BUCKET_NAME }}/myotherfolder |
| 128 | +``` |
| 129 | +
|
| 130 | +All done! |
| 131 | +
|
| 132 | +## Testing |
| 133 | +
|
| 134 | +Some basic test cases below. Add your own too! |
| 135 | +You should look to automate these where possibly. |
| 136 | +I've included my specific tests, and results - some helpful notes in there. |
| 137 | +
|
| 138 | +Ensure success |
| 139 | +- GitHub: edit "view-stack/index.html" |
| 140 | +- Commit to "main" branch |
| 141 | +- Expecting: |
| 142 | + - Build should clearly pass |
| 143 | + - Build should be quick (<10 min) |
| 144 | + - Bucket contents updated |
| 145 | + - Bucket timestamp updated |
| 146 | + - No previous version stored in bucket (we're not versionsing) |
| 147 | +- RESULT: PASS. Took 21 seconds, 18 of those in copying the files. Expect that to grow as files get bigger, but remain under 10 mins. Add a timeout to the GitHub action to enforce that time limit. |
| 148 | +
|
| 149 | +Ensure GitHub Action: only triggers on "main" branch |
| 150 | +- GitHub: make a new branch and commit |
| 151 | +- Expecting: |
| 152 | + - Action does NOT trigger |
| 153 | + - S3 bucket is not updated |
| 154 | +- Because: |
| 155 | + - Should only trigger on "main" branch (ci.yml: on:push:branches: - main) |
| 156 | +- RESULT: PASS |
| 157 | +
|
| 158 | +Ensure Role Policy: fails when has wrong S3 bucket name |
| 159 | +- AWS: edit "GitHubS3DeployPolicy". Change S3 bucket name to something random. |
| 160 | +- Kick off another github workflow on "main" |
| 161 | +- Expecting: |
| 162 | + - Build should clearly fail |
| 163 | + - Nothing redeployed to S3 bucket |
| 164 | +- Because: |
| 165 | + - Role Policy says explicitly which bucket name this role is allowed to deploy into |
| 166 | + - Auth should still work, but aws s3 command should fail with access denied. |
| 167 | +- RESULT: PASS |
| 168 | + - Run aws s3 sync view-stack s3://***/view-stack |
| 169 | + 10fatal error: An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied |
| 170 | + 11Error: Process completed with exit code 1. |
| 171 | +
|
| 172 | +Ensure Role: fails when has wrong GitHub Repo name |
| 173 | +- AWS: edit "GitHubS3DeployRole" Trust Policy. Change GitHub Repo in "token.actions.githubusercontent.com:sub" to something random: "repo:NHSDigitalWRONG/tech-radar:ref:refs/heads/main" |
| 174 | +- Kick off another github workflow on "main" |
| 175 | +- Expecting: |
| 176 | + - Build should clearly fail |
| 177 | + - Nothing redeployed to S3 bucket |
| 178 | +- Because: |
| 179 | + - Role's trust policy says explicitly which GitHub repo is allowed to use this role |
| 180 | +- RESULT: PASS, with notes: |
| 181 | + - Authentication failed: Error: Not authorized to perform sts:AssumeRoleWithWebIdentity |
| 182 | + - But took 2min, seemed to be timing out / retrying |
| 183 | +
|
| 184 | +Ensure Role fails when has wrong GitHub Branch name: |
| 185 | +- AWS: edit "GitHubS3DeployRole" Trust Policy. Change GitHub branch in "token.actions.githubusercontent.com:sub" to something random: "repo:NHSDigital/tech-radar:ref:refs/heads/mainWRONG" |
| 186 | +- Kick off another github workflow on "main" |
| 187 | +- Expecting: |
| 188 | + - Workflow SHOULD still trigger (GitHub action still has "main" as the branch to trigger on) |
| 189 | + - Build should clearly fail |
| 190 | + - Nothing redeployed to S3 bucket |
| 191 | +- Because: |
| 192 | + - Role's trust policy says explicitly which branches of the GitHub repo are allowed to use this role |
| 193 | +- RESULT: PASS, with notes: |
| 194 | + - Authentication failed: Error: Not authorized to perform sts:AssumeRoleWithWebIdentity |
| 195 | + - But took 2min, seemed to be timing out / retrying |
0 commit comments