Tuesday, September 22, 2015

HTML5, AngularJS and hosting on AWS S3 - Oh my!

So I've not done a big "how to" post in a l-o-n-g while, so I thought it'd be useful to document the process of moving from an effectively static ASP.Net MVC web site to an actually static web site that can be hosted directly from an S3 bucket.

Why? Well, my "toy" sites have no real dynamic content, so why maintain a micro-VM on Azure just to host them?

So this will be a step-by-step guide - partly for my own recollection, and also because finding some of the incantations needed to publish a web site successfully to AWS S3 took a fair bit of effort.


Step 0 - Setup


As I'm going to try and maintain these sites 'properly', I'm going to put the source code into GitHub.
joel$ cd Projects/
joel$ mkdir mywebsite.co.uk 
joel$ cd mywebsite.co.uk 
joel$ git init 
Initialized empty Git repository in /Users/joel/Projects/mywebsite.co.uk/.git/
So, I set up a new repository on GitHub with an Apache license and a default README.md file, and connected by empty project folder to that:
joel$ git remote add origin https://github.com/Me/mywebsite.co.uk  
joel$ git pull origin master 
From https://github.com/Me/mywebsite.co.uk * branch            master     -> FETCH_HEAD 
joel$ ls 
LICENSE README.md

Step 1 - Scaffolding

Scaffolding a sensibly structured HTML5/AngularJS site is amazingly easy using Yeoman. A quick check first that we're good to go...


joel$ yo --version && bower --version && grunt --version 
1.3.2 
1.3.12 
grunt-cli v0.1.13

then a whole AngularJS web site scaffolded with one command!
joel$ yo angular  
... 

Commit and push to GitHub gives me a baseline against which I can start working on the site
joel$ git add . 
joel$ git commit -m "Initial scaffolding" 
[master 995f8ba] Initial scaffolding 
 26 files changed, 1640 insertions(+) 
... 

joel$ git push origin master 
... 
To https://github.com/Me/mywebsite.co.uk.git 
   f3525d2..995f8ba  master -> master



Step 2 - Working on the site


I've got to admit I really like the workflow that's enabled by using VSCode and grunt file watching - a quick grunt serve and then just edit and save. With a two monitor setup, this is an absolute dream.

Capturing small changes as individual git commits feels "just right" too.


Step 3 - Setting up publishing to AWS S3


This is where things get interesting. 

Setting up a new bucket in S3 is easy - name the bucket after the web site url (mywebsite.co.uk in this example).

We then need to configure a grunt task to publish to that bucket - Rob Morgan has a very good walkthrough here of how to do this using the grunt-aws package.


joel$ npm install grunt-aws-s3 --save-dev...

And then we add some lines to the Gruntfile.js file:

grunt.loadNpmTasks('grunt-aws-s3'); 
// Configurable paths for the application
var appConfig = {
    app: require('./bower.json').appPath || 'app',
    dist: 'dist',
    s3AccessKey: grunt.option('s3AccessKey') || '',
    s3SecretAccessKey: grunt.option('s3SecretAccessKey') || '',
    s3Bucket: grunt.option('s3Bucket') || 'mywebsite.co.uk',

  };
 
grunt.initConfig({
...
aws_s3: {
            options: {
                accessKeyId: appConfig.s3AccessKey,
                secretAccessKey: appConfig.s3SecretAccessKey,
                bucket: appConfig.s3Bucket,
                region: 'eu-west-1',
            },
            production: {
              files: [
                  { expand: true,
                    dest: '.',
                    cwd: 'dist/',
                    src: ['**'],
                    differential: true }
                    ]
                  }
        }

});
 
grunt.registerTask('deploy', ['build', 'aws_s3']);
Notice that my AWS secrets are injected via grunt command line parameters - so no chance of committing them into GitHub!

Step 4 - Configuring AWS permissions


The biggest headache I found in this whole process was setting AWS permissions up correctly. I don't really want to push via my super-user account, and if I ever get a build server for all this working, I'd rather have a single user per web site with VERY limited permissions to push changes to AWS S3.

Create a deployment user 

In AWS IAM Management, I created a new user called mywebsite.deploy, with an associated Access Key / Secret pair that I downloaded and saved somewhere secure. 

There's no way to get back an access key, so be careful not to forget this step, or you'll have to regenerate the key pair! 


Actually, Amazon recommend rotating keys on a regular basis, so you'll be doing that anyway - but it's still not what you want to be doing every morning before you start.

Create a deployment group

Again in AWS IAM Management, I created a new group called mywebsite_deployment and added the mywebsite.deploy user to that group. 

Next up - permissions.


Grant permissions on the bucket to the group

To do this, we have to add an "Inline policy" to the mywebsite_deploy group to grant basic access to any users in the group. 

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "arn:aws:s3:::mywebsite.co.uk"
    }
  ]
}

Grant restricted rights on the bucket to the deployment user


We don't want the mywebsite.deploy user to be able to do anything to the bucket (such as change permissions), so we restrict their access rights to the bucket contents by applying a policy to the bucket itself

{ "Id": "Policy1438599268262", "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt1438599259521", "Action": [ "s3:DeleteObject", "s3:GetObject", "s3:GetObjectAcl", "s3:PutObject", "s3:PutObjectAcl" ], "Effect": "Allow", "Resource": "arn:aws:s3:::mywebsite.co.uk/*", "Principal": { "AWS": [ "arn:aws:iam::765146773618:user/mywebsite.deploy" ] } } ]}

Step 5 - Deploying to AWS


With all that set up (phew!), then deploying the site to AWS S3 is a one-liner:


joel$ grunt deploy --s3AccessKey=<<your access key>> --s3SecretAccessKey=<<your secret>> 
... 
16/16 objects uploaded to bucket mywebsite.co.uk/ 
Done, without errors.

Step 6 - Set up Static Website Hosting

In the AWS S3 console, select the bucket and click on "Properties" to open the properties pane for the bucket.

Open the "Static Web Site Hosting" section and it's easy to enable hosting just by checking the option. Enter index.html as the default document.

Click "Save", and your content is served from the default endpoint.

Now's a good time to check that your web app runs nicely by just hitting that endpoint in a browser - and get a warm fuzzy feeling.

Step 7 - Domain setup


The last thing to do is to switch over the DNS for the target domain so that www.mywebsite.co.uk is a CNAME for the AWS S3 endpoint.

You can if you want set up AWS CloudFront delivery as well, but that's beyond the scope of this how-to.


3 comments:

David said...

I don't think that this how-to will work if you try to refresh a page not at the root.

Joel Hammond-Turner said...

This how-to is about the publishing of a static HTML web site, so I can't see how whether you can refresh a non-root page is related.

Actually trying to refresh the browser on my speakers page (http://www.hammond-turner.org.uk/#/speaking) works just fine - but that's Angular, not anything to do with the publishing process.

If you're talking about making updates to the site, whether the published site appears to be refreshed is actually down to your Amazon S3 web host and CloudFront settings. If like me you use CloudFront, then there's a significant delay before changes propagate to the leaf nodes. Checking that the direct access URL to the bucket has taken the changes is enough to check all's well initially - but you do have to wait for CloudFront to catch up.

Unknown said...

Ohh. You're using the HashLocationStrategy. I thought that this was Angular2 and you were using PathLocationStrategy by default. I stand corrected.