AWS: Grunt – деплой JS приложения в S3

Автор: | 18/10/2016

aws-logo-square-02Задача: собрать JavaScript (AngularJS + Node.js) приложение, и задеплоить его в корзину S3 для хостинга статического сайта.

Продробнее про S3 – AWS: S3, Simple Storage Service – описание, примеры.

Про хостинг сайтов в S3 – AWS: S3 – хостинг статического сайта.

Сначала – запустим простой “Hello, %username%” в S3.

Затем – соберём имеющееся приложение, и задеплоим его в S3 с помощью Grunt и плагина grunt-aws-s3.

Создание сайта в S3

Создаём корзину:

$ aws s3 mb s3://nodehello.azinchenko.com
make_bucket: s3://nodehello.azinchenko.com/

Настраиваем WebSite для корзины:

$ aws s3 website s3://nodehello.azinchenko.com --index-document index.htm --error-document 4xx.htm

Создаём JSON с описанием политики доступа:

{
  "Version":"2012-10-17",
  "Statement":[{
    "Sid":"PublicReadForGetBucketObjects",
        "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::nodehello.azinchenko.com/*"
      ]
    }
  ]
}

Подключаем:

$ aws s3api put-bucket-policy --bucket nodehello.azinchenko.com --policy file://nodehello.azinchenko.com.json

На настройке домена останавливаться не буду, выглядит он так:

aws_s3_gulp_deploy_1

Запуск AngularJS “Hello, World”

Добавляем проект:

$ mkdir angularhello

В нём создаём файл index.htm с содержимым:

<html>
   
   <head>
      <title>AngularJS First Application</title>
   </head>
   
   <body>
      <h1>Sample Application</h1>
      
      <div ng-app = "">
         <p>Enter your Name: <input type = "text" ng-model = "name"></p>
         <p>Hello <span ng-bind = "name"></span>!</p>
      </div>
      
      <script src = "https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
      
   </body>
</html>

Загружаем в корзину:

$ aws s3 cp index.htm s3://nodehello.azinchenko.com
upload: ./index.htm to s3://nodehello.azinchenko.com/index.htm

Проверяем:

aws_s3_gulp_deploy_2

Grunt deploy

Далее – заделоим приложение с помощью Grunt.

Клонируем репозиторий:

$ git clone -b develop ssh://[email protected]:7999/project/profile-website.git

Его содержимое:

$ ls -l
total 68
-rw-rw-r-- 1 setevoy setevoy   783 жов 17 11:59 bower.json
drwxrwxr-x 5 setevoy setevoy  4096 жов 17 11:59 client
drwxrwxr-x 3 setevoy setevoy  4096 жов 17 11:59 e2e
-rw-rw-r-- 1 setevoy setevoy 19602 жов 17 11:59 Gruntfile.js
-rw-rw-r-- 1 setevoy setevoy  2398 жов 17 11:59 karma.conf.js
-rw-rw-r-- 1 setevoy setevoy  2689 жов 17 11:59 package.json
-rw-rw-r-- 1 setevoy setevoy  1598 жов 17 11:59 protractor.conf.js
-rw-rw-r-- 1 setevoy setevoy  1132 жов 17 11:59 README.md
-rw-rw-r-- 1 setevoy setevoy  8780 жов 17 11:59 README.old.md
drwxrwxr-x 5 setevoy setevoy  4096 жов 17 11:59 server
-rwxrwxr-x 1 setevoy setevoy  1846 жов 17 11:59 setup-build-environment

Описание grunt-aws-s3

Сам деплой в корзину S3 выполняется с помощью плагина grunt-aws-s3, который указан в списке зависимостей в файле package.json:

$ cat package.json | grep grunt-aws
    "grunt-aws-s3": "^0.14.5",

Сборка приложения и деплой включают в себя несколько шагов:

  1. npm install
  2. bower install –allow-root
  3. grunt build:env
  4. grunt deploy –env=ENV –accessKeyId=ABC123 –secretAccessKey=XYZ890

deploy описывается в файле Gruntfile.js:

...
  grunt.registerTask('deploy', function(env, accessKeyId, secretAccessKey) {
    var env = grunt.option('env');
    var accessKeyId = grunt.option('accessKeyId');
    var secretAccessKey = grunt.option('secretAccessKey');
    if (env && accessKeyId && secretAccessKey) {
      grunt.config.set('aws_s3.options.accessKeyId', accessKeyId);
      grunt.config.set('aws_s3.options.secretAccessKey', secretAccessKey);
      return grunt.task.run([
        ('aws_s3:' + env)
      ]);
    }
    else {
      grunt.fail.fatal('Deploy task requires env, accessKeyId and secretAccessKey parameters');
    }
  });
...

А окружения (первый передаваемый аргумент) выглядят так:

...
    aws_s3: {
      options: {
        region: 'eu-west-1',
        uploadConcurrency: 5, // 5 simultaneous uploads
        downloadConcurrency: 5 // 5 simultaneous downloads
      },
      dev: {
        options: {
          bucket: 'profile-dev.domain.com',
          differential: true
        },
        files: [
          {
            expand: true,
            cwd: 'dist/public',
            src: ['**'],
            dest: '/',
            action: 'upload'
          }
        ]
      },
      staging: {
        options: {
          bucket: 'profile-staging.domain.com',
          differential: true
        },
        files: [
          {
            expand: true,
            cwd: 'dist/public',
            src: ['**'],
            dest: '/',
            action: 'upload'
          }
        ]
      },
      production: {
        options: {
          bucket: 'profileapp.domain.com',
          differential: true
        },
        files: [
          {
            expand: true,
            cwd: 'dist/public',
            src: ['**'],
            dest: '/',
            action: 'upload'
          }
        ]
      }
    }

  });
...

Пример деплоя

Настройка корзины

Проверяем имеющиеся корзины:

$ aws s3 ls
...
2016-05-09 19:22:16 profile-dev.domain.com
2016-05-09 21:09:40 profile-staging.domain.com
2016-08-17 16:02:28 profileapp.domain.com
...

Добавляем новую корзину (в регионе eu-west-1, как это указано в aws_s3.options в Grunfile.js:

$ aws s3 mb s3://profile-testing.domain.com --region eu-west-1
make_bucket: s3://profile-testing.domain.com/

Настраиваем её как Website:

$ aws s3 website s3://profile-testing.domain.com --index-document index.html --error-document error.html

Добавляем запись profile-testing.domain.com с алиасом к созданной корзине:

tag_s3_3

Добавляем политику доступа:

$ cat profile-testing.domain.com.json 
{
  "Version":"2012-10-17",
  "Statement":[{
    "Sid":"PublicReadForGetBucketObjects",
        "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::profile-testing.domain.com/*"
      ]
    }
  ]
}

Подключаем:

$ aws s3api put-bucket-policy --bucket profile-testing.domain.com --policy file://profile-testing.domain.com.json

Проверяем:

$ aws s3api get-bucket-policy --bucket profile-testing.domain.com
{
    "Policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"PublicReadForGetBucketObjects\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::profile-testing.domain.com/*\"}]}"
}
Настройка Grunt deploy

В Gruntfile.js – добавляем новое окружение:

...
      prodtesting: {
        options: {
          bucket: 'profile-testing.domain.com',
          differential: true
        },
        files: [
          {
            expand: true,
            cwd: 'dist/public',
            src: ['**'],
            dest: '/',
            action: 'upload'
          }
        ]
      },
...

Добавляем новое окружение с тегом “prodtesting“:

...
      prodtesting: {
        options: {
          process: true,
          data: {
            tagEnvironment: 'prodtesting'
          }
        },
        files: {
          'dist/public/index.html': ['dist/public/index.html']
        }
      },
...
Сборка

Собираем приложение.

Устанавливаем зависимости, описанные в package.json, включая grunt-aws-s3:

$ npm install                                                                                                                                                                                   
npm http GET https://registry.npmjs.org/ejs
npm http GET https://registry.npmjs.org/composable-middleware
npm http GET https://registry.npmjs.org/connect-mongo
...

Устанавливаем зависимости, перечисденные в bower.json:

$ sudo npm install -g bower
$ bower install --allow-root
bower jquery#2.1.4          not-cached https://github.com/jquery/jquery-dist.git#2.1.4
bower jquery#2.1.4             resolve https://github.com/jquery/jquery-dist.git#2.1.4
...

Собираем:

$ sudo npm install -g grunt
npm http GET https://registry.npmjs.org/grunt
npm http 304 https://registry.npmjs.org/grunt
npm http GET https://registry.npmjs.org/grunt/-/grunt-1.0.1.tgz
...
$ grunt build:prodtesting
Running "clean:dist" (clean) task

Running "injector:sass" (injector) task
Missing option `template`, using `dest` as template instead
Injecting scss files (45 files)

Running "concurrent:dist" (concurrent) task
    Warning: Running "sass:server" (sass) task
    Warning: 
    You need to have Ruby and Sass installed and in your PATH for this task to work.
    More info: https://github.com/gruntjs/grunt-contrib-sass
     Use --force to continue.
    
    Aborted due to warnings.
    ...

Упс.

Устанавливаем гемы:

$ sudo npm install -g grunt-contrib-sass
$ sudo gem install sass
Fetching: sass-3.4.22.gem (100%)
Successfully installed sass-3.4.22
1 gem installed
Installing ri documentation for sass-3.4.22...

Билдим ещё раз:

$ grunt build:prodtesting
Running "clean:dist" (clean) task
Cleaning dist/public...OK

Running "injector:sass" (injector) task
Missing option `template`, using `dest` as template instead
Injecting scss files (45 files)
>> Nothing changed

Running "concurrent:dist" (concurrent) task
    
    Running "svgmin:dist" (svgmin) task
    ✔ client/assets/images/ic_watchspecs_line.svg (saved 287 B 41%)
    Total saved: 287 B
    
    Done, without errors. 
...

Проверяем dist/public (указанный как cwd: 'dist/public' в Grunfile.js):

$ ls -l dist/public/
total 24
drwxrwxr-x  2 setevoy setevoy 4096 жов 17 14:03 app
drwxrwxr-x  5 setevoy setevoy 4096 жов 17 14:02 assets
drwxrwxr-x 27 setevoy setevoy 4096 жов 17 14:02 bower_components
-rw-rw-r--  1 setevoy setevoy 1150 жов 17 14:02 favicon.ico
-rw-rw-r--  1 setevoy setevoy 2192 жов 17 14:03 index.html
-rw-rw-r--  1 setevoy setevoy   31 жов 17 14:02 robots.txt
Деплой

Проверяем ещё раз содержимое корзины:

$ aws s3 ls s3://profile-testing.domain.com

Деплоим:

$ grunt deploy --env=prodtesting --accessKeyId=AKIA****U42A --secretAccessKey=FcJMU****A0fL
Running "deploy" task

Running "aws_s3:prodtesting" (aws_s3) task
Uploading to https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/
............................................................................
List: (861 objects):
- dist/public/app/app.css -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/app/app.css
- dist/public/app/app.js -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/app/app.js
- dist/public/app/vendor.js -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/app/vendor.js
- dist/public/assets/fonts/HelveticaNeueLTStd-Bd.eot -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/assets/fonts/HelveticaNeueLTStd-Bd.eot
- dist/public/assets/fonts/HelveticaNeueLTStd-Bd.otf -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/assets/fonts/HelveticaNeueLTStd-Bd.otf
- dist/public/assets/fonts/HelveticaNeueLTStd-Bd.svg -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/assets/fonts/HelveticaNeueLTStd-Bd.svg
- dist/public/assets/fonts/HelveticaNeueLTStd-Bd.ttf -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/assets/fonts/HelveticaNeueLTStd-Bd.ttf
- dist/public/assets/fonts/HelveticaNeueLTStd-Bd.woff -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/assets/fonts/HelveticaNeueLTStd-Bd.woff
....
- dist/public/bower_components/ngAutocomplete/src/ngAutocomplete.js -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/bower_components/ngAutocomplete/src/ngAutocomplete.js
- dist/public/favicon.ico -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/favicon.ico
- dist/public/index.html -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/index.html
- dist/public/robots.txt -> https://s3-eu-west-1.amazonaws.com/profile-testing.domain.com/robots.txt

861/861 objects uploaded to bucket profile-testing.domain.com/

Done, without errors.


Execution Time (2016-10-17 13:18:32 UTC)
aws_s3:prodtesting  2m 34.9s  ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 100%
Total 2m 35.3s

Проверяем:

$ aws s3 ls s3://profile-testing.domain.com
                           PRE app/
                           PRE assets/
                           PRE bower_components/
2016-10-17 16:21:08       1150 favicon.ico
2016-10-17 16:21:08       2192 index.html
2016-10-17 16:21:08         31 robots.txt

Готово.