--- tags: smartcitizen, platform --- # Smart Citizen Platform Deployment [TOC] ## Requirements * We need to have [docker](https://docs.docker.com/) installed. * We need to clone the [`smartcitizen-api`]() repo The platform is run in `docker` containers, and using `docker-compose` to manage them. Depending on the scenario, we will need to change the `docker-compose.yml` file for specifying what containers run. :::warning `docker-compose` has a separate development timeline to `docker`. Now, there is a `compose V2`, which is `docker compose`. In addition, the `docker-compose.yaml` file has different versions, that allow for different configuration options. We currently are with a version in production that has some featured and not others (supports swarm, but no memory limitations, for instance) ::: Workflow (detailed in each case below): 1. Check the `.env` file for understanding what you need to deploy and where 2. Cassandra nodes local or elsewhere? 3. MQTT local or elsewhere? 4. URLs :::info If the infrastructure is deployed in the same provider (i.e. Linode) the IP addresses for each service can be local (i.e, MQTT is in a separate server but the IP address is a local one within linode). For this to work, in Linode, `Add an IP address` to the machine and Select `Privaet`. ::: ## Staging ### Environment This would work for staging: 1. You need to assign some variables in the `.env` file: ``` # STAGING DEFAULT_URL=staging-api.smartcitizen.me RAILS_ENV=production # This is the STAGING server but with its own db! # It needs to be in production mode, not dev # Production config/database.yml DATABASE_URL=postgresql://postgres:postgres@db/sc_final # Tell sidekiq to use hostname redis, not localhost # If this var is set, Redis uses it by default. No config needed REDIS_URL=redis://redis:6379/0/cache REDIS_STORE=redis://redis:6379/3 #Minuteman uses this: REDIC_URL=redis://redis:6379/1 # DISABLE if you don't want staging to send emails #mailgun_api_key=x #mailgun_public_api_key=x #mailgun_domain=mailbot.smartcitizen.me # Not needed (we have external emqx server) # Debug: doco exec mqtt sh; tail -f log/* #EMQX_LOG__LEVEL=error # MQTT Staging (EMQX 5) MQTT_HOST=192.168.134.195 MQTT_CLIENT_ID=smartcitizen-api-staging # This is not needed below (we start one cassandra in ) # New cass cluster, dockerized cass05, cass06 #CASS_HOSTS=109.237.26.252:9160,178.79.189.22:9160 #CASS_HOSTS=109.237.26.252,178.79.189.22 #CASS_HOSTS=178.79.189.22 # Kairos is not enabled in dns for staging. access via ip:port kairos_server=kairos kairos_telnet_port=4242 kairos_port=8080 kairos_http_username=[...] kairos_http_password=[...] # AUTH id.smartcitizen.me AUTH_SECRET_KEY_BASE=somerandomnumberjusttesting #RAILS_SERVE_STATIC_FILES=true RACK_ENV=production PUMA_TOKEN=smartToken # AWS secret aws_access_key=xx aws_secret_key=xx aws_region=eu-west-1 s3_bucket=smartcitizen # Comment below to avoid issues on sentry # RAVEN_DSN_URL=https://x@x.ingest.sentry.io/5196011 ``` 2. In a terminal, run the command to start basic services (`app` and `db`) ``` doco up app db -d ``` This will start the app and the postgres. 3. If running first time, you need to run the following commands inside the app container to start the database: ``` docker-compose exec app bash ``` And then: ``` bin/rails db:create ``` ``` bin/rails db:schema:load ``` ``` bin/rails db:seed ``` 4. Now you can start the other services: ``` doco up -d ``` 5. Checking the output of `doco ps` you should see this: ``` root@staging-api:~/smartcitizen-api# doco ps Name Command State Ports ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- smartcitizen-api_app_1 entrypoint.sh bin/rails se ... Up 0.0.0.0:3000->3000/tcp,:::3000->3000/tcp smartcitizen-api_auth_1 rails server -p 3000 -b 0. ... Up 0.0.0.0:3001->3000/tcp,:::3001->3000/tcp smartcitizen-api_cassandra-1_1 docker-entrypoint.sh cassa ... Up 0.0.0.0:7000->7000/tcp,:::7000->7000/tcp, 0.0.0.0:7001->7001/tcp,:::7001->7001/tcp, 0.0.0.0:7199->7199/tcp,:::7199->7199/tcp, 0.0.0.0:9042->9042/tcp,:::9042->9042/tcp, 0.0.0.0:9160->9160/tcp,:::9160->9160/tcp smartcitizen-api_db_1 docker-entrypoint.sh postgres Up 5432/tcp smartcitizen-api_kairos_1 /usr/bin/runkairos.sh run Up 2003/tcp, 2004/tcp, 0.0.0.0:4242->4242/tcp,:::4242->4242/tcp, 0.0.0.0:8080->8080/tcp,:::8080->8080/tcp smartcitizen-api_mqtt-task_1 entrypoint.sh bundle exec ... Up 3000/tcp smartcitizen-api_push_1 npm start Up 0.0.0.0:8000->8000/tcp,:::8000->8000/tcp smartcitizen-api_redis_1 docker-entrypoint.sh redis ... Up 6379/tcp smartcitizen-api_sidekiq_1 entrypoint.sh bundle exec ... Up 3000/tcp smartcitizen-api_telnet-task_1 entrypoint.sh bundle exec ... Up 3000/tcp smartcitizen-api_web_1 nginx -g daemon off; Up 0.0.0.0:80->80/tcp,:::80->80/tcp, 0.0.0.0:80->80/udp,:::80->80/udp ``` :::info Note that this only deploys the api. It is all new and empty. ::: ### Adding a test device 1. Add an user: ```shell! curl -X POST https://staging-api.smartcitizen.me/v0/users\?email\=eeee@fablabbcn.org\&username\=eeee\&password\=aaaaa ``` Get the token for that user: ```shell! 'https://staging-api.smartcitizen.me/v0/sessions' -X POST -H 'Content-Type: application/json;charset=UTF-8' --data-binary '{"username": "eeee", "password": "ueueueueue"}' {"access_token":"AAAAAAAAAAAAAA"}% ``` 2. Add a kit: * Enter the `rails console`: ```shell! doco exec app rails console DEPRECATION WARNING: Including LoggerSilence is deprecated and will be removed in Rails 6.1. Please use `ActiveSupport::LoggerSilence` instead (called from <main> at /app/config/application.rb:18) initialized a background worker with 2 threads Loading production environment (Rails 6.0.3.3) ... irb(main):001:0> ``` * Create Measurements ```shell Measurement.create!(name: "air temperature", description: "Air temperature is a measure of how hot or cold the air is. It is the most commonly measured weather parameter. Air temperature is dependent on the amount and strength of the sunlight hitting the earth, and atmospheric conditions, such as cloud cover and humidity, which trap heat.") Measurement.create!(name: "relative humidity", description: "Relative humidity is a measure of the amount of moisture in the air relative to the total amount of moisture the air can hold. For instance, if the relative humidity was 50%, then the air is only half saturated with moisture.") Measurement.create!(name: "light", description: "Lux is a measure of how much light is spread over a given area. A full moon clear night is around 1 lux, inside an office building you usually have 400 lux and a bright day can be more than 20000 lux.") Measurement.create!(name: "noise", description: "dB's measure sound pressure difference between the average local pressure and the pressure in the sound wave. A quiet library is below 40dB, your house is around 50dB and a diesel truck in your street 90dB.") Measurement.create!(name: "PM 1", description: "PM stands for particulate matter: the term for a mixture of solid particles and liquid droplets found in the air. Some particles, such as dust, dirt, soot, or smoke, are large or dark enough to be seen with the naked eye.") Measurement.create!(name: "PM 2.5", description: "PM stands for particulate matter: the term for a mixture of solid particles and liquid droplets found in the air. Some particles, such as dust, dirt, soot, or smoke, are large or dark enough to be seen with the naked eye.") Measurement.create!(name: "PM 10", description: "PM stands for particulate matter: the term for a mixture of solid particles and liquid droplets found in the air. Some particles, such as dust, dirt, soot, or smoke, are large or dark enough to be seen with the naked eye.") Measurement.create!(name: "eCO2", description: "Equivalent CO2 is the concentration of CO2 that would cause the same level of radiative forcing as a given type and concentration of greenhouse gas. Examples of such greenhouse gases are methane, perfluorocarbons, and nitrous oxide. CO2 is primarily a by-product of human metabolism and is constantly being emitted into the indoor environment by building occupants. CO2 may come from combustion sources as well. Associations of higher indoor carbon dioxide concentrations with impaired work performance and increased health symptoms have been attributed to correlation of indoor CO2 with concentrations of other indoor air pollutants that are also influenced by rates of outdoor-air ventilation.") Measurement.create!(name: "tVOC", description: "Total volatile organic compounds is a grouping of a wide range of organic chemical compounds to simplify reporting when these are present in ambient air or emissions. Many substances, such as natural gas, could be classified as volatile organic compounds (VOCs).") Measurement.create!(name: "battery", description: "The SCK remaining battery level in percentage.") Measurement.create!(name: "Barometric Pressure", description: "Barometric pressure is the pressure within the atmosphere of Earth. In most circumstances atmospheric pressure is closely approximated by the hydrostatic pressure caused by the weight of air above the measurement point.") ``` * Create Sensors (same as current kit): ``` Sensor.create!(id: 10, name: "Battery SCK", description: "Custom Circuit", unit: "%", measurement_id: 10) Sensor.create!(id: 14, name: "BH1730FVC - Light", description: "Digital Ambient Light Sensor", unit: "lux", measurement_id: 3) Sensor.create!(id: 53, name: "ICS43432 - Noise", description: "I2S Digital Mems Microphone with custom Audio Proc...", unit: "dBA", measurement_id: 4) Sensor.create!(id: 55, name: "SHT31 - Temperature", description: "Temperature", unit: "ºC", measurement_id: 1) Sensor.create!(id: 56, name: "SHT31 - Humidity", description: "Humidity", unit: "%", measurement_id: 2) Sensor.create!(id: 58, name: "MPL3115A2 - Barometric Pressure", description: "Digital Barometric Pressure Sensor", unit: "kPa", measurement_id: 11) Sensor.create!(id: 87, name: "PMS5003 - PM2.5", description: "Particle Matter PM 2.5", unit: "ug/m3", measurement_id: 6) Sensor.create!(id: 88, name: "PMS5003 - PM10", description: "Particle Matter PM 10", unit: "ug/m3", measurement_id: 7) Sensor.create!(id: 89, name: "PMS5003 - PM1.0", description: "Particle Matter PM 1", unit: "ug/m3", measurement_id: 5) Sensor.create!(id: 112, name: "AMS CCS811 - eCO2", description: "Equivalent Carbon Dioxide Digital Indoor Sensor", unit: "ppm", measurement_id: 8) Sensor.create!(id: 113, name: "AMS CCS811 - TVOC", description: "Total Volatile Organic Compounds Digital Indoor Se...", unit: "ppb", measurement_id: 9) ``` * Create Kit ``` Kit.create!(name: "SCK 2.1", description: "Smart Citizen Kit 2.1 with Urban Sensor Board", id: 26, slug: "sck:2,1") ``` * Create components ``` Component.create!(id:1, board_id:26, board_type: "Kit", sensor_id: 10) Component.create!(id:2, board_id:26, board_type: "Kit", sensor_id: 14) Component.create!(id:3, board_id:26, board_type: "Kit", sensor_id: 53) Component.create!(id:4, board_id:26, board_type: "Kit", sensor_id: 55) Component.create!(id:5, board_id:26, board_type: "Kit", sensor_id: 56) Component.create!(id:6, board_id:26, board_type: "Kit", sensor_id: 58) Component.create!(id:7, board_id:26, board_type: "Kit", sensor_id: 87) Component.create!(id:8, board_id:26, board_type: "Kit", sensor_id: 88) Component.create!(id:9, board_id:26, board_type: "Kit", sensor_id: 89) Component.create!(id:10, board_id:26, board_type: "Kit", sensor_id: 112) Component.create!(id:11, board_id:26, board_type: "Kit", sensor_id: 113) ``` * Create Device ``` Device.create!(owner_id:1, name: "Test", kit_id: 26, device_token: "7cfa45", latitude: 41.458030, longitude: 2.211520) ... => #<Device id: 1, owner_id: 1, name: "Test", description: nil, mac_address: nil, latitude: 41.45803, longitude: 2.21152, created_at: "2022-12-12 11:56:35", updated_at: "2022-12-12 11:56:35", kit_id: 26, latest_data: nil, geohash: "sp3ef74qs0", last_recorded_at: nil, meta: nil, location: {"address"=>"Institut Numància, Carrer de Prat de la Riba, Santa Coloma de Gramenet, Barcelonès, Barcelona, Catalonia, 08921, Spain", "city"=>"Santa Coloma de Gramenet", "postal_code"=>"08921", "state_name"=>"Catalonia", "state_code"=>"Catalonia", "country_code"=>"es"}, data: nil, old_data: nil, owner_username: nil, uuid: nil, migration_data: nil, workflow_state: "active", csv_export_requested_at: nil, old_mac_address: nil, state: "never_published", device_token: "7cfa45", hardware_info: nil, notify_stopped_publishing_timestamp: "2019-01-16 16:19:35", notify_low_battery_timestamp: "2019-01-16 16:19:35", notify_low_battery: false, notify_stopped_publishing: false, is_private: false, is_test: false> ``` :::warning Doing `doco start -d` doesn't start kairos properly... `doco ps` ``` root@staging-api:~/smartcitizen-api# doco ps Name Command State Ports --------------------------------------------------------------------------------------- smartcitizen-api_app_1 entrypoint.sh bin/rails Up 0.0.0.0:3000->3000/tcp se ... ,:::3000->3000/tcp smartcitizen-api_auth_1 rails server -p 3000 -b Up 0.0.0.0:3001->3000/tcp 0. ... ,:::3001->3000/tcp smartcitizen- docker-entrypoint.sh Up 0.0.0.0:7000->7000/tcp api_cassandra-1_1 cassa ... ,:::7000->7000/tcp, 0. 0.0.0:7001->7001/tcp,: ::7001->7001/tcp, 0.0. 0.0:7199->7199/tcp,::: 7199->7199/tcp, 0.0.0. 0:9042->9042/tcp,:::90 42->9042/tcp, 0.0.0.0: 9160->9160/tcp,:::9160 ->9160/tcp smartcitizen-api_db_1 docker-entrypoint.sh Up 5432/tcp postgres smartcitizen- /usr/bin/runkairos.sh Exit 0 api_kairos_1 run smartcitizen-api_mqtt- entrypoint.sh bundle Up 3000/tcp task_1 exec ... smartcitizen-api_push_1 npm start Up 0.0.0.0:8000->8000/tcp ,:::8000->8000/tcp smartcitizen- docker-entrypoint.sh Up 6379/tcp api_redis_1 redis ... smartcitizen- entrypoint.sh bundle Up 3000/tcp api_sidekiq_1 exec ... smartcitizen- entrypoint.sh bundle Restarting api_telnet-task_1 exec ... smartcitizen-api_web_1 nginx -g daemon off; Up 0.0.0.0:80->80/tcp,::: 80->80/tcp, 0.0.0.0:80 ->80/udp,:::80->80/udp root@staging-api:~/smartcitizen-api# doco start kairos Starting kairos ... done ``` ::: ### Test basic requests * Get historical readings from device ``` https://staging-api.smartcitizen.me/v0/devices/1/readings?sensor_id=55&rollup=1m ``` ## Resources limitations :::warning Currently we can't put in _resources limitations_ because do not use the right compose file format. We need to update docker-compose (standalone). **Be careful** now there is a change and docker also has compose in it in the new releases ([Compose V2](https://docs.docker.com/compose/reference/)) ::: :::danger This might not work in production so **easily** because we have a more complex system! ::: ### Upgrade `docker-compose` 1. Rename `docker-compose` just in case ``` $ mv /usr/bin/docker-compose /usr/bin/docker-compose.bak ``` 2. Get new `docker-compose` standalone: https://docs.docker.com/compose/install/other/ 3. Add symbolic link ``` $ ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose ``` 4. Test: ``` $ docker-compose --version Docker Compose version v2.14.0 ``` ### Update `docker-compose.yml` file :::info No need to specify anymore the compose file version at the top ::: Updating **only** the `app` container: ``` app: build: context: . # Skip installing development & test gems in production, saves 20s build time. # If developing with Docker, this line might need to be commented out. args: - BUNDLE_WITHOUT=test development env_file: .env ports: - "3000:3000" depends_on: # We disable some containers in production - db - auth - redis - sidekiq - mqtt-task - telnet-task - push #- mqtt restart: always deploy: resources: limits: memory: 2gb volumes: - "./:/app" #command: rails server -p 3000 -b '0.0.0.0' #command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" environment: # For this to take effect, remove init/timeout, and in Gemfile, fix "rack-timeout" to not require anything. RACK_TIMEOUT_SERVICE_TIMEOUT: 25 logging: driver: "json-file" options: max-size: "100m" ``` And running: ``` docker-compose up -d ``` Now we should see limitations in the memory usage of the container: ``` CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 012cebc5c96e smartcitizen-api-web-1 0.00% 2.625MiB / 3.84GiB 0.07% 1.47kB / 0B 365kB / 0B 2 1f69e8ce224d smartcitizen-api-app-1 0.04% 124.5MiB / **2GiB** 6.08% 1.58kB / 0B 9.91MB / 4.1kB 14 9fac3eb24614 smartcitizen-api-kairos-1 0.15% 162.4MiB / 3.84GiB 4.13% 48.1kB / 29.7kB 12.5MB / 77.8kB 45 28aedc5aafce smartcitizen-api-telnet-task-1 0.00% 0B / 0B 0.00% 0B / 0B 0B / 0B 0 c58670cc61ad smartcitizen-api-redis-1 0.15% 2.426MiB / 3.84GiB 0.06% 604kB / 521kB 3.95MB / 0B 4 83ffce7c1113 smartcitizen-api-db-1 0.03% 24.74MiB / 3.84GiB 0.63% 135kB / 605kB 2.77MB / 963kB 8 53df592cea94 smartcitizen-api-cassandra-1-1 0.50% 1.184GiB / 3.84GiB 30.84% 31.1kB / 47.6kB 6.22MB / 3.08MB 58 b507b1bba92e smartcitizen-api-push-1 0.00% 43.02MiB / 3.84GiB 1.09% 2.41MB / 12.4kB 258kB / 16.4kB 23 83a83f57caea smartcitizen-api-sidekiq-1 0.17% 182.2MiB / 3.84GiB 4.63% 162kB / 192kB 3.96MB / 6.89MB 31 b7280385023c smartcitizen-api-auth-1 0.00% 61.2MiB / 3.84GiB 1.56% 1.8kB / 0B 2MB / 0B 6 097bc0751948 smartcitizen-api-mqtt-task-1 0.06% 159.1MiB / 3.84GiB 4.05% 637kB / 545kB 4.72MB / 6.89MB 4 ``` :::warning We may want to disable `swap` usage in production to avoid problems? This is done in `mqtt-staging` as per recommendation of `emqx` docs, and in `api-staging` for testing purposes... ::: ## Security