DEV Community 👩‍💻👨‍💻

Cover image for Deploy multientorno con gitlab-ci y surge.sh
Óscar Medina
Óscar Medina

Posted on

Deploy multientorno con gitlab-ci y surge.sh


Mantener aplicaciones web en las que tenemos que manualmente subir a los servidores de un hosting o ejecutar una serie de comandos en el terminal puede resultar bastante tedioso. Además, si le añadimos el hecho de querer tener dos entornos donde uno de ellos nos servirá para realizar las pruebas, la cosa se complica.
Y es aquí donde la integración continua nos puede ayudar mucho a concentrar nuestro trabajo en lo que realmente importa.

Hay servicios en la nube que nos ayudan a gestionar este tipo de
tareas como kubernetes en servicios como amazon web services o
semejantes, siempre y cuando estemos dispuestos a costear sus
servicios.

En este ejemplo vamos a aprender como hacer el deploy de nuestra aplicacion en dos entornos separados y de forma muy sencilla y económica.

Primeros pasos

Lo primero que tendremos que tener en cuenta es la configuración de nuestro proyecto, para este ejemplo usaremos webpack 4, para ello necesitaremos los siguientes ficheros.

|_config
  |__development.json
  |__staging.json
  |__production.json
|_webpack.dev.js
|_webpack.prod.js
|_webpack.common.js
|_package.json
|_ postcss.config.js

Ahora vamos a los ficheros de configuracion base de nuestro proyecto, para este caso vamos a usar react 16 transpilado con babel y para los estilos sass/scss, la configuracion puede variar en funcion de tus necesidades.
El contenido del package.json será el siguiente.

{
  "name": "my-awesome-project",
  "version": "1.0.1",
  "private": true,
  "scripts": {
    "dev": "export NODE_ENV=development && export env=development && webpack-dev-server --hot --inline --config webpack.dev.js",
    "prebuild": "rm -rf ./build/*",
    "prebuild-qa": "rm -rf ./build/*",
    "build": "export NODE_ENV=production && export env=production && webpack --config webpack.prod.js -p",
    "build-qa": "export NODE_ENV=staging && export env=staging && webpack --config webpack.prod.js -p"
  },
  "devDependencies": {
    "@babel/core": "^7.0.0",
    "@babel/plugin-proposal-class-properties": "^7.0.0",
    "@babel/plugin-proposal-decorators": "^7.0.0",
    "@babel/plugin-proposal-do-expressions": "^7.0.0",
    "@babel/plugin-proposal-export-default-from": "^7.0.0",
    "@babel/plugin-proposal-export-namespace-from": "^7.0.0",
    "@babel/plugin-proposal-function-bind": "^7.0.0",
    "@babel/plugin-proposal-function-sent": "^7.0.0",
    "@babel/plugin-proposal-json-strings": "^7.0.0",
    "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0",
    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0",
    "@babel/plugin-proposal-numeric-separator": "^7.0.0",
    "@babel/plugin-proposal-optional-chaining": "^7.0.0",
    "@babel/plugin-proposal-pipeline-operator": "^7.0.0",
    "@babel/plugin-proposal-throw-expressions": "^7.0.0",
    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
    "@babel/plugin-syntax-import-meta": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@babel/preset-es2015": "^7.0.0-beta.53",
    "@babel/preset-react": "^7.0.0",
    "babel-core": "^7.0.0-bridge.0",
    "babel-eslint": "^9.0.0",
    "babel-jest": "^23.4.2",
    "babel-loader": "^8.0.0",
    "css-loader": "^0.28.7",
    "extract-text-webpack-plugin": "^2.1.2",
    "file-loader": "^1.1.6",
    "html-webpack-plugin": "^3.2.0",
    "isomorphic-fetch": "^2.2.1",
    "mini-css-extract-plugin": "^0.5.0",
    "node-sass": "^4.9.2",
    "nodemon": "^1.18.3",
    "postcss-loader": "^2.1.6",
    "redux-logger": "^3.0.6",
    "redux-mock-store": "^1.4.0",
    "redux-thunk": "^2.3.0",
    "sass-loader": "^7.0.3",
    "script-ext-html-webpack-plugin": "^2.0.1",
    "style-loader": "^0.19.1",
    "surge": "^0.20.1",
    "uglifyjs-webpack-plugin": "^2.0.1",
    "url-loader": "^0.6.2",
    "webpack": "^4.5.0",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.14"
  },
  "dependencies": {
    "prop-types": "^15.6.0",
    "react": "^16.8.3",
    "react-dom": "^16.8.3",
    "react-router": "^4.3.1",
    "react-router-dom": "^4.3.1"

  }
}

Una de las partes mas importantes de tener en cuenta es usar variables de entorno y para ello tenemos los ficheros ubicados en la carpeta config (Si vamos a publicar este proyecto esta carpeta deberia estar ignorada, ya que aquí es donde ponemos los token o todo lo que sea necesario para el funcionamiento de nuestro proyecto)

// Todos los ficheros seguiran esta estructura
{
    "API_BASE_PATH": "http://localhost:8080",
    "env": "development",
    //añade tantas variables como te sea necesario
}

Y ahora vamos con la configuracion de webpack.

// webpack.dev.js
const projectName = 'My awesome project'
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const commonRules = require('./webpack.common');

const APP_DIR = path.resolve(__dirname, './src/');
const entryJS = path.resolve(APP_DIR, 'index.jsx');
const entryCss = path.resolve(APP_DIR, 'sass/index.scss');

module.exports = {
    mode: 'development',
    entry: [entryJS/*, entryCss*/],
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.[hash].js',
        publicPath: '/'
    },
    module: {
        rules: [
            ...commonRules,
            { // sass / scss loader for webpack
                test: /\.(sass|scss)$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: {
                            sourceMap: true,
                            minimize: true
                        } // translates CSS into CommonJS
                    },
                    {
                        loader: 'postcss-loader',
                        options: {
                            sourceMap: true
                        }
                    },
                    {
                        loader: 'sass-loader',
                        options: {
                            sourceMap: false
                        }
                    }
                ],
            }
        ]
    },
    resolve: {
        modules: [APP_DIR, 'node_modules'],
        extensions: ['.js', '.json', '.jsx', '.css'],
    },
    performance: {
        maxEntrypointSize: 400000, // int (in bytes)
        assetFilter: function(assetFilename) {
            // Function predicate that provides asset filenames
            return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
        }
    },
    target: 'web',
    devtool: 'source-map',
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        compress: true,
        port: 8082,
        hot: true,
        historyApiFallback: true
    },
    plugins: [
        /*new MiniCssExtractPlugin({
            filename: 'bundle.[hash].css',
        }),*/
        new HtmlWebpackPlugin({
            title: projectName,
            template: path.join(__dirname, 'src', 'index.html'),
            filename: 'index.html',
        }),
        new webpack.EnvironmentPlugin({
            ...require('./config/' + (process.env.env || 'development') + '.json' )
        })
    ]
};
// webpack.dev.js
const projectName = 'My awesome project'
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const webpack = require('webpack');

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const commonRules = require('./webpack.common');

const APP_DIR = path.resolve(__dirname, './src/');
const entryJS = path.resolve(APP_DIR, 'index.jsx');
const entryCss = path.resolve(APP_DIR, 'sass/index.scss');

console.log(require(`./config/${process.env.env}.json`))

module.exports = {
    mode: 'production',
    entry: [entryJS/*, entryCss*/],
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.[hash].js',
        publicPath: '/'
    },
    module: {
        rules: [
            ...commonRules,
            { // sass / scss loader for webpack
                test: /\.(sass|scss)$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],
            },
        ]
    },
    resolve: {
        modules: [APP_DIR, 'node_modules'],
        extensions: ['.js', '.json', '.jsx', '.css'],
    },
    performance: {
        hints: 'warning', // enum    maxAssetSize: 200000, // int (in bytes),
        maxEntrypointSize: 400000, // int (in bytes)
        assetFilter: function(assetFilename) {
            // Function predicate that provides asset filenames
            return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
        }
    },
    target: 'web',
    plugins: [
        new webpack.EnvironmentPlugin({
            ...require(`./config/${process.env.env}.json`)
        }),
        /*new MiniCssExtractPlugin({
            filename: 'bundle.[hash].css',
        }),*/
        new HtmlWebpackPlugin({
            title: projectName,
            template: path.join(__dirname, 'src', 'index.html'),
            filename: '200.html'
        }),
        new UglifyJsPlugin({

        })
    ]
};
// webpack.common.js
const path = require('path');

const APP_DIR = path.resolve(__dirname, './src/');

module.exports =  [
    {
        test: /\.(jsx|js)?$/,
        exclude: /node_modules/,
        include: APP_DIR,
        loader: 'babel-loader'
    },
    {
        test: /\.css$/,
        use: [
            { loader: 'style-loader' },
            { loader: 'css-loader' }
        ]
    },
    {
        test: /\.(jpe?g|gif|png|svg|woff|eot|ttf|wav|mp3)$/,
        use: [
            {loader: 'file-loader'}
        ]
    }
];
// este fichero es la configuracion de post css, nos evita la necesidad de añadir prefijos de navegador para distintas etiquetas css. (Webkit, moz, etc.)
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
};

Con esto, y teniendo en cuenta los entry points del proyecto que son los que podemos ver en los ficheros dev y prod que son:

const APP_DIR = path.resolve(__dirname, './src/');
const entryJS = path.resolve(APP_DIR, 'index.jsx');
const entryCss = path.resolve(APP_DIR, 'sass/index.scss');

Debemos o bien satisfacer las necesidades de webpack o bien configurarlo para que se adapte a nuestras necesidades.

Esta configuración esta preparada para ser ejecutada sobre un terminal
unix, como puede ser el caso de linux, mac de forma nativa o windows
bajo el subsistema linux u otros emuladores de terminal linux.

Una vez que tenemos nuestra aplicacion funcionando es hora de pasar a la acción con gitlab-ci, importante tener una cuenta de gitlab y usarla como repositorio del proyecto.

Integración continua con gitlab

La integración continua de gitlab nos va a facilitar la tarea de hacer el deploy en nuestra plataforma de hosting en nuestro caso surge y vamos a poder tener un registro de los depoys y su estado y para ello solo vamos a necesitar añadir un fichero a nuestro proyecto.
Antes de poder añadir la integracion continua necesitamos añadir el token de surge para poder hacer el deploy en nuestro dominio, para ello podemos utilizar el comando ¨surge token¨, nos solicitara que iniciemos sesión o nos registremos si no lo estamos.
Obteniendo el token de surge

Una vez tengamos el token debemos ir a la configuracion de gitlab en la pestaña CI/CD y añadimos las siguientes variables de entorno:
SURGE_TOKEN: Es el token que acabamos de conseguir.
SURGE_LOGIN: Es nuestro correo elecrtrónico.
Una vez hecho ya vamos a por el ultimo paso añadir la integración continua a nuestro repositorio.

// .gitlab-ci.yml
image: node:10.15.1
stages:
  - staging
  - production

staging:
  stage: staging
  script:
    - npm install --silent
    - npm run build-qa
    - echo "staging.domain.com" > build/CNAME
    - node_modules/.bin/surge build
  only:
    - develop
    - schedules

production:
  stage: production
  script:
    - npm install --silent
    - npm run build
    - echo "www.domain.com" > build/CNAME
    - node_modules/.bin/surge build
  only:
    - master
    - schedules

Conclusión

Una vez terminemos con todos los pasos tendremos 2 ramas de nuestro proyecto que estan conectadas a la integración continua, por lo que podremos tener nuestras nuevas funtionalidades en un entorno de pruebas antes de pasarlas a produccion y sin gastar un solo euro. En todo caso, necesitaremos un dominio si queremos tener nuestras apps un dominio propio en vez de el que surge nos proportion ¨domain.surge.sh¨.

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.