Thursday, April 06, 2023

First Time Provisioning Setup for Rails

 #cloud-config

apt:

  sources:

    nodesource:

      source: "deb https://deb.nodesource.com/node_14.x $RELEASE main"

      keyid: "1655A0AB68576280"

    yarn:

      source: "deb https://dl.yarnpkg.com/debian/ stable main"

      keyid: "23E7166788B63E1E"


packages:

  - curl

  - git

  - build-essential

  - zlib1g-dev

  - libssl-dev

  - libreadline-dev

  - libyaml-dev

  - libsqlite3-dev

  - sqlite3

  - libxml2-dev

  - libxslt1-dev

  - libcurl4-openssl-dev

  - software-properties-common

  - libffi-dev

  - nodejs

  - yarn


runcmd:

  - sudo apt-add-repository -y ppa:rael-gc/rvm

  - sudo apt-get update

  - sudo apt-get install -y rvm

  - echo 'source "/etc/profile.d/rvm.sh"' >> ~/.bashrc

  - source ~/.bashrc

  - rvm install ruby

  - rvm use ruby --default

  - gem install bundler

  - gem install rails


final_message: "Rails stack is ready!"



Cloud-init is a versatile tool that helps you automate the configuration of new cloud instances. When using cloud-init, it's important to follow best practices to ensure your instances are configured efficiently, securely, and reliably. Here are some best practices for creating a cloud-init.yaml file:


Keep it modular and maintainable: Split your cloud-init.yaml into multiple files or sections to make it more modular and easier to maintain. Use the include directive to import other files, allowing you to organize your code and reuse common configurations.


Limit the use of scripts: Whenever possible, use cloud-init's built-in modules and directives to handle tasks like package installation and user management, rather than using custom scripts. This will make your configuration more declarative and easier to understand.


Use cloud-config syntax: The cloud-config syntax is more readable and easier to manage than shell scripts. It also ensures that actions are executed in the correct order, making your configuration more reliable.


Secure sensitive data: Avoid placing sensitive data like passwords or API keys directly in the cloud-init.yaml file. Instead, use a secure method to store and retrieve this information, such as environment variables, secrets management tools, or cloud provider-specific features.


Validate the syntax: Before applying your cloud-init.yaml file, validate the syntax using a YAML validator or a cloud-init validation tool like cloud-init devel schema to catch any syntax errors or issues.


Test and iterate: Test your cloud-init.yaml file on a non-critical environment before deploying it to production instances. This will help you identify and fix any issues or errors before they impact your production environment.


Document your configuration: Add comments to your cloud-init.yaml file to explain the purpose of each section, directive, or script. This will make it easier for others (or yourself) to understand and maintain your configuration in the future.


Keep your configuration up-to-date: Regularly review and update your cloud-init.yaml file to ensure it reflects the current state of your infrastructure and follows the latest best practices.


Use version control: Store your cloud-init.yaml file in a version control system like Git to track changes over time and collaborate with others.


Make it idempotent: Whenever possible, ensure that your cloud-init.yaml file can be run multiple times without causing unintended side effects. This can help prevent configuration drift and make it easier to recover from errors.


Let's apply additional best practices to the cloud-init YAML files I provided earlier.


Add a header to each file for better readability and understanding.


Use the package module and the user module to manage users and groups.


Use the write_files module with proper permissions for creating files.


Here's the revised version of the files:


packages.yaml



# Install necessary packages

# Filename: packages.yaml


packages:

  - postgresql

  - postgresql-contrib

  - libpq-dev

  - redis-server

  - build-essential

  - nodejs

  - yarn


postgres.yaml


# Set up PostgreSQL

# Filename: postgres.yaml


write_files:

  - content: |

      CREATE DATABASE myapp_production;

      CREATE USER myapp WITH PASSWORD 'secure_password';

      GRANT ALL PRIVILEGES ON DATABASE myapp_production TO myapp;

    path: /tmp/setup_postgres.sql

    permissions: '0644'


runcmd:

  - sudo -u postgres psql -f /tmp/setup_postgres.sql




ruby.yaml


# Install Ruby and Bundler

# Filename: ruby.yaml


runcmd:

  - curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-installer | bash

  - echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc

  - echo 'eval "$(rbenv init -)"' >> ~/.bashrc

  - source ~/.bashrc

  - rbenv install 2.7.4

  - rbenv global 2.7.4

  - gem install bundler



rails_app.yaml


# Set up your Rails application

# Filename: rails_app.yaml


runcmd:

  - git clone https://github.com/your-username/your-rails-app.git /home/ubuntu/your-rails-app

  - cd /home/ubuntu/your-rails-app

  - bundle install

  - RAILS_ENV=production bundle exec rails db:migrate

  - RAILS_ENV=production bundle exec rails assets:precompile



sidekiq.yaml


# Set up Sidekiq as a systemd service

# Filename: sidekiq.yaml


write_files:

  - content: |

      [Unit]

      Description=Sidekiq Background Worker

      After=network.target


      [Service]

      Type=simple

      WorkingDirectory=/home/ubuntu/your-rails-app

      ExecStart=/bin/bash -lc 'bundle exec sidekiq -e production'

      Restart=always

      RestartSec=3

      User=ubuntu

      Group=ubuntu


      [Install]

      WantedBy=multi-user.target

    path: /tmp/sidekiq.service

    permissions: '0644'


runcmd:

  - sudo mv /tmp/sidekiq.service /etc/systemd/system/sidekiq.service

  - sudo systemctl enable sidekiq.service

  - sudo systemctl start sidekiq.service


And finally, the main cloud-init.yaml file:


#cloud-config


# Update package lists and upgrade packages

package_upgrade: true


# Include separate files

include:

  - packages.yaml

  - postgres.yaml

  - ruby.yaml

  - rails_app.yaml

  - sidekiq.yaml


These revisions apply better practices by adding headers, using proper modules for package installation and user management, and setting appropriate file permissions.




To ensure that all services start automatically when the server is rebooted, we need to enable them using the systemctl enable command. I've made the necessary changes to the YAML files. The only change required is in the sidekiq.yaml file:


sidekiq.yaml


# Set up Sidekiq as a systemd service

# Filename: sidekiq.yaml


write_files:

  - content: |

      [Unit]

      Description=Sidekiq Background Worker

      After=network.target


      [Service]

      Type=simple

      WorkingDirectory=/home/ubuntu/your-rails-app

      ExecStart=/bin/bash -lc 'bundle exec sidekiq -e production'

      Restart=always

      RestartSec=3

      User=ubuntu

      Group=ubuntu


      [Install]

      WantedBy=multi-user.target

    path: /tmp/sidekiq.service

    permissions: '0644'


runcmd:

  - sudo mv /tmp/sidekiq.service /etc/systemd/system/sidekiq.service

  - sudo systemctl enable sidekiq.service

  - sudo systemctl start sidekiq.service



The main cloud-init.yaml file remains the same:


cloud-init.yaml:


#cloud-config


# Update package lists and upgrade packages

package_upgrade: true


# Include separate files

include:

  - packages.yaml

  - postgres.yaml

  - ruby.yaml

  - rails_app.yaml

  - sidekiq.yaml


Now, the Sidekiq service will automatically start when the server is rebooted. Other services like PostgreSQL and Redis are already set to start automatically on boot by default when installed via package management.


You can create a separate cloud-init config file for the security updates and include it in your main cloud-init.yaml file using the include directive.


Create a separate file for security updates, let's call it security_updates.yaml:


# security_updates.yaml

package_upgrade: true

packages:

  - unattended-upgrades

  - update-notifier-common

write_files:

  - path: /etc/apt/apt.conf.d/20auto-upgrades

    content: |

      APT::Periodic::Update-Package-Lists "1";

      APT::Periodic::Download-Upgradeable-Packages "1";

      APT::Periodic::AutocleanInterval "7";

      APT::Periodic::Unattended-Upgrade "1";

  - path: /etc/apt/apt.conf.d/50unattended-upgrades

    content: |

      Unattended-Upgrade::Allowed-Origins {

          "${distro_id}:${distro_codename}-security";

      };


Now, update your main cloud-init.yaml file and include the security_updates.yaml file using the include directive:


#cloud-config

include:

  - security_updates.yaml


# Rest of the main cloud-init configuration


This way, you can maintain the security updates configuration separately and include it in the main cloud-init file when needed. This is especially helpful if you want to maintain modular and reusable configuration files.


#cloud-config
include:
  - security_updates.yaml

# Add the deploy user with sudo privileges
users:
  - name: deploy
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: sudo, adm
    shell: /bin/bash

# Rest of the main cloud-init configuration

Setup ssh-authorized-keys using Ansible playbook when customizing the prebuilt image for the customer.