# January 9th, 2020

# Continuous Deployment with CircleCI and s3deploy

Now I have a simple to edit, static site that meets my résumé hosting and blogging needs; but deployment is currently a bit tedious. Manually copying the built files into the S3 bucket and invalidating the CloudFront distribution takes too much time and requires too much fiddling with my mouse to encourage consistent updates. 😉

So, what am I after? I think a desirable workflow looks something like:

  1. yarn docs:dev
  2. Write a blog post
  3. Commit and push the repository to GitHub
  4. yarn docs:build is run automatically
  5. The output (docs/.vuepress/dist/) of the build is uploaded to AWS S3
  6. The CloudFront distribution in front of the S3 bucket is invalidated

Steps 1, 2, and 3 are already doable; so what about 4, 5, and 6?

Sounds like a job for CircleCI! CircleCI integrates with our existing GitHub repository to provide a configurable build / deploy pipeline as a service. It also offers a free plan which will work for us in this case.

In order to perform the AWS operations of syncing the bucket and invalidating the distribution, I've chosen to use the often recommended bep/s3deploy script. It's kind of like aws s3 sync, but optimized for static sites to modify only the minimal set of files.

Setting up CircleCI is as simple as making an account and using their interface to set up a new project tracking our GitHub repository. Once that's complete, we will need to create .circleci/config.yml in the root of the repository; but before we get to editing it we need to satisfy a prerequisite first: creating a deployment service account.

# AWS IAM User

First we need to create an IAM User in AWS that will represent the service account under which s3deploy will do its work.

Add a user with the "Programmatic access" type. Click "Next: Permissions", select "Attach existing policies directly", and then click on the "Create policy" button.

The policy that I'm using looks like:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::www.ethanaa.com"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "arn:aws:s3:::www.ethanaa.com/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:GetDistribution",
                "cloudfront:CreateInvalidation"
            ],
            "Resource": "*"
        }
    ]
}
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

It allows s3deploy to fetch the information that it needs about the state of the S3 bucket, insert / update / delete bucket contents, and invalidate our CloudFront distribution (CDN cache) so that our changes are immediately available.

Make sure to download / copy the credentials for the new user to a file. We'll need both the AWS_ACCESS_KEY_ID and the AWS_SECRET_ACCESS_KEY for the configuration of our CircleCI project's build environment.

# CircleCI

Set both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to the values associated with the newly created deployment service account IAM User.

CircleCI Project Build Environment

Now edit .circleci/config.yml:

# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2.1

defaults: &defaults
  working_directory: ~/project
  docker:
    - image: circleci/node:latest

jobs:
  #------------------------------------------------------------
  # 1. Install dependencies
  #------------------------------------------------------------

  install-dependencies:
    <<: *defaults
    steps:
      - checkout

      - restore_cache:
          keys:
            - v1-deps-{{ checksum "yarn.lock" }}
            - v1-deps

      - run:
          name: "Install yarn dependencies"
          command: yarn --frozen-lockfile --non-interactive

      - run:
          name: "Install s3deploy"
          command: curl -L https://github.com/bep/s3deploy/releases/download/v2.3.2/s3deploy_2.3.2_Linux-64bit.tar.gz | tar xvz

      - save_cache:
          key: v1-deps-{{ checksum "yarn.lock" }}
          paths:
            - ~/.cache/yarn

      - persist_to_workspace:
          root: ~/project
          paths:
            - node_modules
            - s3deploy

  #------------------------------------------------------------
  # 2. Build VuePress
  #------------------------------------------------------------

  build:
    <<: *defaults
    steps:
      - checkout
      - attach_workspace:
          at: ~/project
      - run:
          name: "Run build"
          command: yarn docs:build
      - run:
          name: "Copy resume pdf into dist"
          command: cp Ethan_Anderson_Resume.pdf docs/.vuepress/dist/
      - persist_to_workspace:
          root: ~/project
          paths:
            - docs/.vuepress/dist

  #------------------------------------------------------------
  # 3. Deploy to S3
  #------------------------------------------------------------

  deploy:
    <<: *defaults
    steps:
      - attach_workspace:
          at: ~/project
      - run:
          name: "Deploy to S3"
          command: |
            if [ "${CIRCLE_BRANCH}" = "master" ]; then
              cd ~/project/docs/.vuepress/dist
              ~/project/s3deploy -region=us-east-1 -bucket=www.ethanaa.com -distribution-id=E272FZWDOFIWKK -v
            else
              echo "Non-master branch: dry run only"
              echo
              echo "Working directory files:"
              pwd
              echo
              ls -lAF
              cd ~/project/docs/.vuepress/dist
              echo "Built files:"
              pwd
              echo
              ls -lAF
              echo
              ~/project/s3deploy -region=us-east-1 -bucket=www.ethanaa.com -distribution-id=E272FZWDOFIWKK -v -try
            fi

#------------------------------------------------------------
# Workflows
#------------------------------------------------------------

workflows:
  version: 2
  build:
    jobs:
      - install-dependencies
      - build: {requires: [install-dependencies]}
      - deploy: {requires: [build]}
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

I've separated the config into 3 jobs: install-dependencies, build, and deploy.

First the dependencies are installed by running yarn as well as downloading and extracting the latest release of s3deploy. Then yarn docs:build is run and my résumé is copied from the root directory into docs/.vuepress/dist/ so that it's processed by s3deploy. Finally, s3deploy is executed targeting our S3 bucket and CloudFormation distribution.

If you push to any other branch than master then s3deploy will be run with -try; resulting in a dry-run execution that will not modify any AWS objects.

So, let's commit the code and push it up to GitHub to trigger a build!

Last Updated: 1/12/2020, 8:59:38 PM