diff --git a/.github/workflows/gateway_commons.yml b/.github/workflows/gateway_commons.yml index 6f1e33d0a..6015b406c 100644 --- a/.github/workflows/gateway_commons.yml +++ b/.github/workflows/gateway_commons.yml @@ -30,8 +30,8 @@ jobs: EOF ) matrix=$(echo $matrixSource | jq --arg branchName "$branchName" 'map(. | select((.branch==$branchName)) )') - echo ::set-output name=matrix::{\"include\":$(echo $matrix)}\" - echo ::set-output name=matrixLength::$(echo $matrix | jq length) + echo "matrix={\"include\":$(echo $matrix)}" >> $GITHUB_OUTPUT + echo "matrixLength=$(echo $matrix | jq length)" >> $GITHUB_OUTPUT deploy: name: Deploy Gateway Commons diff --git a/.github/workflows/public_gateway.yml b/.github/workflows/public_gateway.yml index 2f634564d..b1effd03a 100644 --- a/.github/workflows/public_gateway.yml +++ b/.github/workflows/public_gateway.yml @@ -33,7 +33,6 @@ jobs: "ecs_service": "public-gateway-staging", "ecs_cluster": "somleng-switch-staging", "deploy": false - }, { "identifier": "public-gateway", diff --git a/.github/workflows/services.yml b/.github/workflows/services.yml index 149c5da44..202a478c7 100644 --- a/.github/workflows/services.yml +++ b/.github/workflows/services.yml @@ -74,8 +74,8 @@ jobs: EOF ) matrix=$(echo $matrixSource | jq --arg branchName "$branchName" 'map(. | select((.branch==$branchName)) )') - echo ::set-output name=matrix::{\"include\":$(echo $matrix)}\" - echo ::set-output name=matrixLength::$(echo $matrix | jq length) + echo "matrix={\"include\":$(echo $matrix)}" >> $GITHUB_OUTPUT + echo "matrixLength=$(echo $matrix | jq length)" >> $GITHUB_OUTPUT deploy: name: Deploy @@ -102,15 +102,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Create Sentry release - uses: getsentry/action-release@v1 - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: somleng - SENTRY_PROJECT: somleng-switch-services - with: - environment: ${{ matrix.environment }} - - name: Configure AWS credentials id: aws-login uses: aws-actions/configure-aws-credentials@v4 @@ -160,3 +151,12 @@ jobs: --image-uri ${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} \ --architectures "arm64" \ --publish + + - name: Create Sentry release + uses: getsentry/action-release@v1 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: somleng + SENTRY_PROJECT: somleng-switch-services + with: + environment: ${{ matrix.environment }} diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 07c47494a..63bb6c7a2 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -8,6 +8,7 @@ jobs: outputs: matrix: ${{ steps.set-deployment-matrix.outputs.matrix }} matrixLength: ${{ steps.set-deployment-matrix.outputs.matrixLength }} + deployMatrix: ${{ steps.set-deployment-matrix.outputs.deployMatrix }} defaults: run: working-directory: components/app @@ -77,11 +78,12 @@ jobs: EOF ) matrix=$(echo $matrixSource | jq --arg branchName "$branchName" 'map(. | select((.branch==$branchName)) )') - echo ::set-output name=matrix::{\"include\":$(echo $matrix)}\" - echo ::set-output name=matrixLength::$(echo $matrix | jq length) + echo "matrix={\"include\":$(echo $matrix)}" >> $GITHUB_OUTPUT + echo "matrixLength=$(echo $matrix | jq length)" >> $GITHUB_OUTPUT + echo "deployMatrix={\"region\":[\"ap-southeast-1\",\"us-east-1\"],\"include\":$(echo $matrix)}" >> $GITHUB_OUTPUT - deploy: - name: Deploy + build-packages: + name: Build Packages runs-on: ubuntu-latest needs: - build @@ -105,15 +107,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Create Sentry release - uses: getsentry/action-release@v1 - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: somleng - SENTRY_PROJECT: somleng-switch - with: - environment: ${{ matrix.environment }} - - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -190,7 +183,7 @@ jobs: ${{ env.FREESWITCH_EVENT_LOGGER_ECR_REPOSITORY_URI }}:${{ env.IMAGE_TAG }} ${{ env.FREESWITCH_EVENT_LOGGER_GHCR_REPOSITORY_URI }}:${{ matrix.image_tag }} - - name: Build and push App + - name: Build and push Switch App uses: docker/build-push-action@v6 with: context: components/app @@ -203,6 +196,37 @@ jobs: ${{ env.APP_ECR_REPOSITORY_URI }}:${{ env.IMAGE_TAG }} ${{ env.APP_GHCR_REPOSITORY_URI }}:${{ matrix.image_tag }} + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: + - build + - build-packages + env: + IMAGE_TAG: ${{ github.sha }} + APP_ECR_REPOSITORY_URI: public.ecr.aws/somleng/somleng-switch + NGINX_ECR_REPOSITORY_URI: public.ecr.aws/somleng/somleng-switch-nginx + FREESWITCH_ECR_REPOSITORY_URI: public.ecr.aws/somleng/somleng-switch-freeswitch + FREESWITCH_EVENT_LOGGER_ECR_REPOSITORY_URI: public.ecr.aws/somleng/somleng-switch-freeswitch-event-logger + + strategy: + matrix: ${{fromJSON(needs.build.outputs.deployMatrix)}} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials + id: aws-login + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + role-skip-session-tagging: true + role-duration-seconds: 3600 + aws-region: ${{ matrix.region }} + - name: Get current task definition run: | aws ecs describe-task-definition --task-definition "${{ matrix.identifier }}" --query 'taskDefinition' > task-definition.json @@ -239,10 +263,33 @@ jobs: container-name: app image: ${{ env.APP_ECR_REPOSITORY_URI }}:${{ env.IMAGE_TAG }} - - name: Deploy App Server + - name: Deploy Switch uses: aws-actions/amazon-ecs-deploy-task-definition@v2 with: task-definition: ${{ steps.render-app-task-def.outputs.task-definition }} service: ${{ matrix.ecs_service }} cluster: ${{ matrix.ecs_cluster }} wait-for-service-stability: true + + release: + name: Release + runs-on: ubuntu-latest + needs: + - build + - deploy + + strategy: + matrix: ${{fromJson(needs.build.outputs.matrix)}} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create Sentry release + uses: getsentry/action-release@v1 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: somleng + SENTRY_PROJECT: somleng-switch + with: + environment: ${{ matrix.environment }} diff --git a/components/app/config/app_settings.yml b/components/app/config/app_settings.yml index 4d3d81fdd..e3c21c96c 100644 --- a/components/app/config/app_settings.yml +++ b/components/app/config/app_settings.yml @@ -11,18 +11,19 @@ default: &default redis_url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> redis_pool_size: <%= ENV.fetch('DB_POOL') { 250 } %> services_function_arn: <%= ENV.fetch('SERVICES_FUNCTION_ARN') { "arn:aws:lambda:ap-southeast-1:12345:function:function-name" } %> + services_function_region: <%= ENV.fetch('SERVICES_FUNCTION_REGION') { "ap-southeast-1" } %> production: &production <<: *default sentry_dsn: "<%= AppSettings.credentials.fetch('sentry_dsn') %>" ahn_core_password: "<%= ENV.fetch('AHN_CORE_PASSWORD') %>" ahn_http_password: "<%= AppSettings.credentials.fetch('ahn_http_password') %>" - call_platform_host: "https://api.internal.somleng.org" + call_platform_host: "https://api.somleng.org" call_platform_password: "<%= AppSettings.credentials.fetch('call_platform_password') %>" staging: <<: *production - call_platform_host: "https://api-staging.internal.somleng.org" + call_platform_host: "https://api-staging.somleng.org" development: &development <<: *default diff --git a/components/app/config/initializers/services.rb b/components/app/config/initializers/services.rb index aae55440b..b1cfcab05 100644 --- a/components/app/config/initializers/services.rb +++ b/components/app/config/initializers/services.rb @@ -1,3 +1,4 @@ Services.configure do |config| config.function_arn = AppSettings.fetch(:services_function_arn) + config.function_region = AppSettings.fetch(:services_function_region) end diff --git a/components/app/lib/services/client.rb b/components/app/lib/services/client.rb index 44dc7b12a..f22a3ba12 100644 --- a/components/app/lib/services/client.rb +++ b/components/app/lib/services/client.rb @@ -2,8 +2,8 @@ module Services class Client attr_reader :lambda_client - def initialize(lambda_client: Aws::Lambda::Client.new) - @lambda_client = lambda_client + def initialize(**options) + @lambda_client = options.fetch(:lambda_client) { default_client } end def build_client_gateway_dial_string(username:, destination:) @@ -26,5 +26,9 @@ def invoke_lambda(payload) ) JSON.parse(response.payload.read) end + + def default_client + Aws::Lambda::Client.new(region: Services.configuration.function_region) + end end end diff --git a/components/app/lib/services/configuration.rb b/components/app/lib/services/configuration.rb index 5ce9076c9..f51b544f0 100644 --- a/components/app/lib/services/configuration.rb +++ b/components/app/lib/services/configuration.rb @@ -1,5 +1,5 @@ module Services class Configuration - attr_accessor :function_arn + attr_accessor :function_arn, :function_region end end diff --git a/components/gateway/public_gateway/opensips.cfg b/components/gateway/public_gateway/opensips.cfg index 89a1039f4..6c21ea3b9 100644 --- a/components/gateway/public_gateway/opensips.cfg +++ b/components/gateway/public_gateway/opensips.cfg @@ -198,6 +198,11 @@ route{ exit; } + if ( get_source_group( $var(group)) ) { + # do something with $var(group) + xlog("group is $var(group)\n"); + }; + # Some UAC send a Route Header # with a local proxy IP in an initial INVITE Request # According to https://opensips.org/html/docs/modules/3.4.x/rr.html#func_loose_route @@ -229,18 +234,18 @@ route{ xlog("L_NOTICE", "Load balancing request on port $rp\n"); if ($rp == "SIP_PORT") { - xlog("L_NOTICE", "Starting LB with resources: gw\n"); + xlog("L_NOTICE", "Starting LB on group $var(group) with resources: gw\n"); - if ( !lb_start(1,"gw")) { + if ( !lb_start($var(group),"gw")) { send_reply(500,"No Destination available"); exit; } } if ($rp == "SIP_ALTERNATIVE_PORT") { - xlog("L_NOTICE", "Starting LB with resources: gwalt\n"); + xlog("L_NOTICE", "Starting LB on group $var(group) with resources: gwalt\n"); - if ( !lb_start(1,"gwalt")) { + if ( !lb_start($var(group),"gwalt")) { send_reply(500,"No Destination available"); exit; } diff --git a/components/services/.rubocop.yml b/components/services/.rubocop.yml new file mode 100644 index 000000000..1248a2f82 --- /dev/null +++ b/components/services/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: ../../.rubocop.yml diff --git a/components/services/Gemfile b/components/services/Gemfile index 5b270282a..547f6b370 100644 --- a/components/services/Gemfile +++ b/components/services/Gemfile @@ -7,6 +7,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "aws-sdk-ec2" gem "aws-sdk-ecs" gem "aws-sdk-ssm" +gem "base64" gem "ox" # XML parser. required by aws-sdk-s3 gem "pg" gem "sentry-ruby" diff --git a/components/services/Gemfile.lock b/components/services/Gemfile.lock index 1e38baaa5..333fb5748 100644 --- a/components/services/Gemfile.lock +++ b/components/services/Gemfile.lock @@ -2,28 +2,29 @@ GEM remote: https://rubygems.org/ specs: aws-eventstream (1.3.0) - aws-partitions (1.970.0) - aws-sdk-core (3.203.0) + aws-partitions (1.973.0) + aws-sdk-core (3.204.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-ec2 (1.472.0) + aws-sdk-ec2 (1.473.0) aws-sdk-core (~> 3, >= 3.203.0) aws-sigv4 (~> 1.5) - aws-sdk-ecs (1.155.0) + aws-sdk-ecs (1.156.0) aws-sdk-core (~> 3, >= 3.203.0) aws-sigv4 (~> 1.5) - aws-sdk-ssm (1.176.0) + aws-sdk-ssm (1.177.0) aws-sdk-core (~> 3, >= 3.203.0) aws-sigv4 (~> 1.5) aws-sigv4 (1.9.1) aws-eventstream (~> 1, >= 1.0.2) + base64 (0.2.0) bigdecimal (3.1.8) coderay (1.1.3) concurrent-ruby (1.3.4) diff-lcs (1.5.1) - docile (1.4.0) + docile (1.4.1) jmespath (1.6.2) method_source (1.1.0) ox (2.14.18) @@ -32,15 +33,14 @@ GEM coderay (~> 1.1) method_source (~> 1.0) rake (13.2.1) - rexml (3.3.6) - strscan + rexml (3.3.7) rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.0) + rspec-core (3.13.1) rspec-support (~> 3.13.0) - rspec-expectations (3.13.0) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-mocks (3.13.1) @@ -59,9 +59,8 @@ GEM simplecov-cobertura (2.1.0) rexml simplecov (~> 0.19) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) - strscan (3.1.0) PLATFORMS ruby @@ -70,6 +69,7 @@ DEPENDENCIES aws-sdk-ec2 aws-sdk-ecs aws-sdk-ssm + base64 ox pg pry @@ -81,4 +81,4 @@ DEPENDENCIES simplecov-cobertura BUNDLED WITH - 2.5.11 + 2.5.18 diff --git a/components/services/app.rb b/components/services/app.rb index a9693e5d8..fa98ec975 100644 --- a/components/services/app.rb +++ b/components/services/app.rb @@ -38,7 +38,7 @@ def process def handle_ecs_event(event) case event.group when ENV.fetch("SWITCH_GROUP") - HandleSwitchEvent.call(event:) + HandleSwitchEvent.call(event:, regions: SomlengRegion::Region) when ENV.fetch("MEDIA_PROXY_GROUP") HandleMediaProxyEvent.call(event:) when ENV.fetch("CLIENT_GATEWAY_GROUP") diff --git a/components/services/app/jobs/create_opensips_permission_job.rb b/components/services/app/jobs/create_opensips_permission_job.rb index fa18c6585..6b3fc162f 100644 --- a/components/services/app/jobs/create_opensips_permission_job.rb +++ b/components/services/app/jobs/create_opensips_permission_job.rb @@ -1,14 +1,15 @@ class CreateOpenSIPSPermissionJob - attr_reader :source_ip + attr_reader :source_ip, :group_id - def initialize(source_ip) + def initialize(source_ip, options = {}) @source_ip = source_ip + @group_id = options.fetch("group_id", 0) end def call return if OpenSIPSAddress.exists?(ip: source_ip, database_connection:) - OpenSIPSAddress.new(ip: source_ip, database_connection:).save! + OpenSIPSAddress.new(ip: source_ip, grp: group_id, database_connection:).save! end private diff --git a/components/services/app/jobs/update_opensips_permission_job.rb b/components/services/app/jobs/update_opensips_permission_job.rb new file mode 100644 index 000000000..0ed35fc24 --- /dev/null +++ b/components/services/app/jobs/update_opensips_permission_job.rb @@ -0,0 +1,18 @@ +class UpdateOpenSIPSPermissionJob + attr_reader :source_ip, :group_id + + def initialize(source_ip, options = {}) + @source_ip = source_ip + @group_id = options.fetch("group_id", 0) + end + + def call + OpenSIPSAddress.where(ip: source_ip, database_connection:).update(grp: group_id) + end + + private + + def database_connection + DatabaseConnections.find(:public_gateway) + end +end diff --git a/components/services/app/models/application_record.rb b/components/services/app/models/application_record.rb index fced72ee0..7aa60b2b1 100644 --- a/components/services/app/models/application_record.rb +++ b/components/services/app/models/application_record.rb @@ -2,12 +2,12 @@ class ApplicationRecord class << self attr_accessor :table_name - def exists?(database_connection:, **query) - where(database_connection:, **query).count.positive? + def exists?(**) + where(**).count.positive? end - def where(database_connection:, **query) - table(database_connection:).where(query) + def where(database_connection:, **) + table(database_connection:).where(**) end private diff --git a/components/services/app/parsers/ecs_event_parser.rb b/components/services/app/parsers/ecs_event_parser.rb index 53135da32..177061a06 100644 --- a/components/services/app/parsers/ecs_event_parser.rb +++ b/components/services/app/parsers/ecs_event_parser.rb @@ -12,6 +12,7 @@ class ECSEventParser :public_ip, :group, :event_type, + :region, keyword_init: true ) @@ -33,7 +34,8 @@ def parse_event eni_private_ip:, private_ip:, public_ip:, - group: + group:, + region: ) end @@ -73,6 +75,7 @@ def eni_private_ip_details def private_ip return eni_private_ip unless eni_private_ip.nil? + ec2_instance_private_ip unless container_instance_arn.nil? end @@ -104,13 +107,19 @@ def cluster_arn detail.fetch("clusterArn") end + def region + event.fetch("region") + end + def container_instance_details - @container_instance_details ||= ecs_client.describe_container_instances( - cluster: cluster_arn, - container_instances: [ - container_instance_arn - ] - ).to_h + @container_instance_details ||= with_aws_client(ecs_client, region:) do |client| + client.describe_container_instances( + cluster: cluster_arn, + container_instances: [ + container_instance_arn + ] + ).to_h + end end def ec2_instance_id @@ -118,9 +127,11 @@ def ec2_instance_id end def ec2_instance_details - @ec2_instance_details ||= ec2_client.describe_instances( - instance_ids: [ ec2_instance_id ] - ).to_h + @ec2_instance_details ||= with_aws_client(ec2_client, region:) do |client| + client.describe_instances( + instance_ids: [ ec2_instance_id ] + ).to_h + end end def ec2_instance_private_ip @@ -130,4 +141,9 @@ def ec2_instance_private_ip def ec2_instance_public_ip ec2_instance_details.dig(:reservations, 0, :instances, 0, :public_ip_address) end + + def with_aws_client(client, region:) + client.config.region = region + yield(client) + end end diff --git a/components/services/app/workflows/handle_switch_event.rb b/components/services/app/workflows/handle_switch_event.rb index 6680662fe..049e4f563 100644 --- a/components/services/app/workflows/handle_switch_event.rb +++ b/components/services/app/workflows/handle_switch_event.rb @@ -1,13 +1,14 @@ class HandleSwitchEvent < ApplicationWorkflow - attr_reader :event + attr_reader :event, :regions - def initialize(event:) + def initialize(event:, regions:) @event = event + @regions = regions end def call if event.task_running? && event.eni_attached? - load_balancer_manager.create_targets + load_balancer_manager.create_targets(group_id: load_balancer_group) elsif event.task_stopped? && event.eni_deleted? load_balancer_manager.delete_targets end @@ -15,6 +16,10 @@ def call private + def load_balancer_group + regions.find_by!(identifier: event.region).group_id + end + def load_balancer_manager @load_balancer_manager ||= ManageLoadBalancerTargets.new(ip_address: event.private_ip) end diff --git a/components/services/app/workflows/manage_load_balancer_targets.rb b/components/services/app/workflows/manage_load_balancer_targets.rb index 7334f556f..bacab2b88 100644 --- a/components/services/app/workflows/manage_load_balancer_targets.rb +++ b/components/services/app/workflows/manage_load_balancer_targets.rb @@ -5,11 +5,11 @@ def initialize(ip_address:) @ip_address = ip_address end - def create_targets + def create_targets(**) gateway_databases.each do |database_connection| database_connection.transaction do load_balancer_targets.each do |load_balancer_target| - create_opensips_load_balancer_target!(load_balancer_target:, database_connection:) + create_opensips_load_balancer_target!(load_balancer_target:, database_connection:, **) end end end @@ -26,7 +26,7 @@ def delete_targets private - def create_opensips_load_balancer_target!(load_balancer_target:, database_connection:) + def create_opensips_load_balancer_target!(load_balancer_target:, database_connection:, **attributes) return if OpenSIPSLoadBalancerTarget.exists?(dst_uri: load_balancer_target.dst_uri, database_connection:) OpenSIPSLoadBalancerTarget.new( @@ -34,7 +34,8 @@ def create_opensips_load_balancer_target!(load_balancer_target:, database_connec resources: load_balancer_target.resources, group_id: 1, probe_mode: 2, - database_connection: + database_connection:, + **attributes ).save! end diff --git a/components/services/config/app_settings.yml b/components/services/config/app_settings.yml index 21144db55..cd613d04e 100644 --- a/components/services/config/app_settings.yml +++ b/components/services/config/app_settings.yml @@ -1,4 +1,6 @@ default: &default + region_data: '<%= ENV.fetch("REGION_DATA", "{}") %>' + stub_regions: false production: &production <<: *default @@ -9,6 +11,7 @@ staging: development: &development <<: *default + stub_regions: true test: <<: *development diff --git a/components/services/config/application.rb b/components/services/config/application.rb index 4bc2ba77b..d442dfc79 100644 --- a/components/services/config/application.rb +++ b/components/services/config/application.rb @@ -1,9 +1,9 @@ require_relative "app_settings" require_relative "initializers/aws_stubs" -Dir["#{File.dirname(__FILE__)}/../lib/**/*.rb"].sort.each { |f| require f } +Dir["#{File.dirname(__FILE__)}/../lib/**/*.rb"].each { |f| require f } EncryptedEnvironmentVariables.new.decrypt -Dir["#{File.dirname(__FILE__)}/**/*.rb"].sort.each { |f| require f } -Dir["#{File.dirname(__FILE__)}/../app/**/*.rb"].sort.each { |f| require f } +Dir["#{File.dirname(__FILE__)}/**/*.rb"].each { |f| require f } +Dir["#{File.dirname(__FILE__)}/../app/**/*.rb"].each { |f| require f } diff --git a/components/services/config/initializers/somleng_region.rb b/components/services/config/initializers/somleng_region.rb new file mode 100644 index 000000000..e9c7e7330 --- /dev/null +++ b/components/services/config/initializers/somleng_region.rb @@ -0,0 +1,6 @@ +require "json" + +SomlengRegion.configure do |config| + config.region_data = AppSettings.fetch(:region_data) + config.stub_regions = AppSettings.fetch(:stub_regions) +end diff --git a/components/services/lib/somleng_region.rb b/components/services/lib/somleng_region.rb new file mode 100644 index 000000000..726bff620 --- /dev/null +++ b/components/services/lib/somleng_region.rb @@ -0,0 +1,17 @@ + +module SomlengRegion + class << self + def configure + yield(configuration) + configuration + end + + def configuration + @configuration ||= Configuration.new + end + alias config configuration + end +end + +require_relative "somleng_region/configuration" +require_relative "somleng_region/region" diff --git a/components/services/lib/somleng_region/configuration.rb b/components/services/lib/somleng_region/configuration.rb new file mode 100644 index 000000000..c7c63acdd --- /dev/null +++ b/components/services/lib/somleng_region/configuration.rb @@ -0,0 +1,5 @@ +module SomlengRegion + class Configuration + attr_accessor :region_data, :stub_regions + end +end diff --git a/components/services/lib/somleng_region/region.rb b/components/services/lib/somleng_region/region.rb new file mode 100644 index 000000000..ac4d0c473 --- /dev/null +++ b/components/services/lib/somleng_region/region.rb @@ -0,0 +1,50 @@ +require "ostruct" + +module SomlengRegion + class Region < OpenStruct + class RegionNotFound < StandardError; end + + MOCK_REGIONS = [ + new( + identifier: "ap-southeast-1", + alias: "hydrogen", + group_id: 1, + human_name: "South East Asia (Singapore)", + nat_ip: "13.250.230.15" + ), + new( + identifier: "us-east-1", + alias: "helium", + group_id: 2, + human_name: "North America (N. Virginia, USA)", + nat_ip: "52.4.242.134" + ) + ] + + class << self + def all + collection + end + + def find_by(attributes) + collection.find do |region| + attributes.all? { |key, value| region[key] == value } + end + end + + def find_by!(*) + find_by(*) || raise(RegionNotFound.new) + end + + private + + def collection + @collection ||= config.stub_regions ? MOCK_REGIONS : config.region_data.map { |region| new(region) } + end + + def config + SomlengRegion.configuration + end + end + end +end diff --git a/components/services/spec/parsers/ecs_event_parser_spec.rb b/components/services/spec/parsers/ecs_event_parser_spec.rb index d19dbde20..6854fd4dc 100644 --- a/components/services/spec/parsers/ecs_event_parser_spec.rb +++ b/components/services/spec/parsers/ecs_event_parser_spec.rb @@ -111,6 +111,17 @@ ) end + it "requests to the correct regional endpoint" do + ecs_client, ec2_client = stub_aws_clients(region: "ap-southeast-1") + event = build_ecs_event_payload(region: "us-east-1") + parser = ECSEventParser.new(event, ecs_client:, ec2_client:) + + parser.parse_event + + expect(ecs_client.api_requests.first.fetch(:context).client.config.region).to eq("us-east-1") + expect(ec2_client.api_requests.first.fetch(:context).client.config.region).to eq("us-east-1") + end + it "handles public instances" do ecs_client, ec2_client = stub_aws_clients(private_ip_address: "10.0.0.1", public_ip_address: "54.251.92.249") event = build_ecs_event_payload @@ -126,6 +137,7 @@ def stub_aws_clients(options = {}) ecs_client = Aws::ECS::Client.new( + region: options.fetch(:region, "ap-southeast-1"), stub_responses: { describe_container_instances: { container_instances: [ @@ -138,6 +150,7 @@ def stub_aws_clients(options = {}) ) ec2_client = Aws::EC2::Client.new( + region: options.fetch(:region, "ap-southeast-1"), stub_responses: { describe_instances: { reservations: [ diff --git a/components/services/spec/requests/ecs_events_spec.rb b/components/services/spec/requests/ecs_events_spec.rb index 234cce5c9..413e5e6e0 100644 --- a/components/services/spec/requests/ecs_events_spec.rb +++ b/components/services/spec/requests/ecs_events_spec.rb @@ -4,6 +4,7 @@ it "handles switch events" do stub_env("SWITCH_GROUP" => "service:somleng-switch") payload = build_ecs_event_payload( + region: "us-east-1", group: "service:somleng-switch", eni_private_ip: "10.1.1.100", eni_status: "ATTACHED", @@ -14,6 +15,9 @@ expect(public_gateway_load_balancer.count).to eq(2) expect(client_gateway_load_balancer.count).to eq(2) + expect(public_gateway_load_balancer.first).to include( + group_id: 2 + ) end it "handles client gateway events" do diff --git a/components/services/spec/requests/sqs_message_event_spec.rb b/components/services/spec/requests/sqs_message_event_spec.rb index 5f9767437..0ba8e1215 100644 --- a/components/services/spec/requests/sqs_message_event_spec.rb +++ b/components/services/spec/requests/sqs_message_event_spec.rb @@ -6,7 +6,7 @@ event_source_arn: "arn:aws:sqs:us-east-2:123456789012:somleng-switch-permissions", body: { "job_class" => "CreateOpenSIPSPermissionJob", - "job_args" => [ "165.57.32.1" ] + "job_args" => [ "165.57.32.1", { "group_id" => 1 } ] }.to_json ) @@ -16,7 +16,7 @@ expect(result.count).to eq(1) expect(result[0]).to include( ip: "165.57.32.1", - grp: 0, + grp: 1, mask: 32, port: 0, proto: "any" @@ -39,6 +39,27 @@ expect(address.count).to eq(0) end + it "updates an address message", :public_gateway do + create_address(ip: "165.57.32.1", grp: 2) + + payload = build_sqs_message_event_payload( + event_source_arn: "arn:aws:sqs:us-east-2:123456789012:somleng-switch-permissions", + body: { + "job_class" => "UpdateOpenSIPSPermissionJob", + "job_args" => [ "165.57.32.1", { "group_id" => 1 } ] + }.to_json + ) + + invoke_lambda(payload:) + + result = address.all + expect(result.count).to eq(1) + expect(result[0]).to include( + ip: "165.57.32.1", + grp: 1 + ) + end + it "adds a subscriber record", :client_gateway do payload = build_sqs_message_event_payload( event_source_arn: "arn:aws:sqs:us-east-2:123456789012:somleng-switch-permissions", diff --git a/components/services/spec/support/event_helpers.rb b/components/services/spec/support/event_helpers.rb index 85ad70231..07ffb1088 100644 --- a/components/services/spec/support/event_helpers.rb +++ b/components/services/spec/support/event_helpers.rb @@ -25,6 +25,7 @@ def build_sqs_message_event_payload(data = {}) def build_ecs_event_payload(data = {}) data = { + region: "us-west-2", eni_private_ip: "10.0.0.1", eni_status: "ATTACHED", last_status: "RUNNING", @@ -51,6 +52,7 @@ def build_ecs_event_payload(data = {}) payload = JSON.parse(file_fixture("task_state_change_event.json").read) overrides = { + "region" => data.fetch(:region), "detail" => { "attachments" => data.fetch(:attachments), "lastStatus" => data.fetch(:last_status), diff --git a/components/services/spec/support/factory_helpers.rb b/components/services/spec/support/factory_helpers.rb index 0aa471e84..4195b2617 100644 --- a/components/services/spec/support/factory_helpers.rb +++ b/components/services/spec/support/factory_helpers.rb @@ -4,8 +4,8 @@ def create_load_balancer_target(dst_uri:, resources:) client_gateway_database_connection.table(:load_balancer).insert(dst_uri:, resources:) end - def create_address(ip:) - public_gateway_database_connection.table(:address).insert(ip:) + def create_address(**) + public_gateway_database_connection.table(:address).insert(**) end def create_rtpengine_target(socket:) diff --git a/components/testing/tests/public_gateway/inbound_test.sh b/components/testing/tests/public_gateway/inbound_test.sh index 04506ac12..7bf5d58ff 100755 --- a/components/testing/tests/public_gateway/inbound_test.sh +++ b/components/testing/tests/public_gateway/inbound_test.sh @@ -15,8 +15,8 @@ media_server="$(dig +short freeswitch)" public_gateway="$(dig +short public_gateway)" reset_db -create_load_balancer_entry "gw" "5060" -create_address_entry $(hostname -i) +create_load_balancer_entry "gw" "5060" "2" +create_address_entry "$(hostname -i)" "2" reload_opensips_tables sipp -sf $scenario public_gateway:5060 -s 1234 -m 1 -trace_msg > /dev/null diff --git a/components/testing/tests/public_gateway/support/test_helpers.sh b/components/testing/tests/public_gateway/support/test_helpers.sh index 0d4d16d97..8495620da 100755 --- a/components/testing/tests/public_gateway/support/test_helpers.sh +++ b/components/testing/tests/public_gateway/support/test_helpers.sh @@ -21,6 +21,8 @@ reload_opensips_tables () { create_address_entry () { ip="$1" + grp="$2" + grp="${grp:=1}" - psql -q $DATABASE_URL -c "INSERT INTO address (ip) VALUES('$ip');" + psql -q $DATABASE_URL -c "INSERT INTO address (ip, grp) VALUES('$ip', '$grp');" } diff --git a/components/testing/tests/support/test_helpers.sh b/components/testing/tests/support/test_helpers.sh index f39892042..03ab57689 100755 --- a/components/testing/tests/support/test_helpers.sh +++ b/components/testing/tests/support/test_helpers.sh @@ -5,8 +5,10 @@ set -e create_load_balancer_entry () { gateway_identifier="$1" port="$2" + group_id="$3" + group_id="${group_id:=1}" psql -q $DATABASE_URL \ - -c "INSERT INTO load_balancer (group_id, dst_uri, resources, probe_mode) VALUES('1', 'sip:freeswitch:$port', '$gateway_identifier=fs://:secret@freeswitch:8021', 2);" + -c "INSERT INTO load_balancer (group_id, dst_uri, resources, probe_mode) VALUES('$group_id', 'sip:freeswitch:$port', '$gateway_identifier=fs://:secret@freeswitch:8021', 2);" } assert_in_file () { diff --git a/infrastructure/core/terraform.tf b/infrastructure/core/terraform.tf index 3242599d3..7cf9efcd0 100644 --- a/infrastructure/core/terraform.tf +++ b/infrastructure/core/terraform.tf @@ -12,6 +12,6 @@ provider "aws" { } provider "aws" { - region = "us-east-1" - alias = "us-east-1" + region = "us-east-1" + alias = "us-east-1" } diff --git a/infrastructure/modules/client_gateway/autoscaling.tf b/infrastructure/modules/client_gateway/autoscaling.tf new file mode 100644 index 000000000..d43449657 --- /dev/null +++ b/infrastructure/modules/client_gateway/autoscaling.tf @@ -0,0 +1,25 @@ +resource "aws_appautoscaling_policy" "policy" { + name = "client_gateway-scale" + service_namespace = aws_appautoscaling_target.scale_target.service_namespace + resource_id = aws_appautoscaling_target.scale_target.resource_id + scalable_dimension = aws_appautoscaling_target.scale_target.scalable_dimension + policy_type = "TargetTrackingScaling" + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "ECSServiceAverageCPUUtilization" + } + + target_value = 30 + scale_in_cooldown = 300 + scale_out_cooldown = 60 + } +} + +resource "aws_appautoscaling_target" "scale_target" { + service_namespace = "ecs" + resource_id = "service/${var.ecs_cluster.name}/${aws_ecs_service.this.name}" + scalable_dimension = "ecs:service:DesiredCount" + min_capacity = var.min_tasks + max_capacity = var.max_tasks +} diff --git a/infrastructure/modules/somleng_switch/aws.tf b/infrastructure/modules/client_gateway/aws.tf similarity index 100% rename from infrastructure/modules/somleng_switch/aws.tf rename to infrastructure/modules/client_gateway/aws.tf diff --git a/infrastructure/modules/client_gateway/cloudwatch.tf b/infrastructure/modules/client_gateway/cloudwatch.tf new file mode 100644 index 000000000..4c1f177d2 --- /dev/null +++ b/infrastructure/modules/client_gateway/cloudwatch.tf @@ -0,0 +1,4 @@ +resource "aws_cloudwatch_log_group" "this" { + name = var.identifier + retention_in_days = 7 +} diff --git a/infrastructure/modules/client_gateway/container_instances.tf b/infrastructure/modules/client_gateway/container_instances.tf new file mode 100644 index 000000000..b026941d4 --- /dev/null +++ b/infrastructure/modules/client_gateway/container_instances.tf @@ -0,0 +1,23 @@ +module "container_instances" { + source = "../container_instances" + + app_identifier = var.identifier + vpc = var.vpc + instance_subnets = var.vpc.public_subnets + associate_public_ip_address = true + max_capacity = var.max_tasks * 2 + cluster_name = var.ecs_cluster.name + security_groups = [var.db_security_group.id] + user_data = var.assign_eips ? [ + { + path = "/opt/assign_eip.sh", + content = templatefile( + "${path.module}/templates/assign_eip.sh", + { + eip_tag = var.identifier + } + ), + permissions = "755" + } + ] : [] +} diff --git a/infrastructure/modules/client_gateway/ecs.tf b/infrastructure/modules/client_gateway/ecs.tf new file mode 100644 index 000000000..6f0df14a4 --- /dev/null +++ b/infrastructure/modules/client_gateway/ecs.tf @@ -0,0 +1,147 @@ +resource "aws_ecs_capacity_provider" "this" { + name = var.identifier + + auto_scaling_group_provider { + auto_scaling_group_arn = module.container_instances.autoscaling_group.arn + managed_termination_protection = "ENABLED" + managed_draining = "ENABLED" + + managed_scaling { + maximum_scaling_step_size = 1000 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 100 + } + } +} + +resource "aws_ecs_task_definition" "this" { + family = var.identifier + network_mode = "host" + requires_compatibilities = ["EC2"] + execution_role_arn = aws_iam_role.task_execution_role.arn + container_definitions = jsonencode([ + { + name = "client_gateway", + image = "${var.app_image}:latest", + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.this.name, + awslogs-region = var.aws_region, + awslogs-stream-prefix = var.app_environment + } + }, + essential = true, + portMappings = [ + { + containerPort = var.sip_port, + hostPort = var.sip_port, + protocol = "udp" + }, + { + containerPort = var.sip_port, + hostPort = var.sip_port, + protocol = "tcp" + } + ], + healthCheck = { + command = ["CMD-SHELL", "nc -z -w 5 $(hostname -i) $SIP_PORT"], + interval = 10, + retries = 10, + timeout = 5 + }, + mountPoints = [ + { + sourceVolume = "opensips", + containerPath = "/var/opensips" + } + ], + secrets = [ + { + name = "DATABASE_PASSWORD", + valueFrom = var.db_password_parameter.arn + } + ], + environment = [ + { + name = "FIFO_NAME", + value = var.opensips_fifo_name, + }, + { + name = "DATABASE_NAME", + value = var.db_name + }, + { + name = "DATABASE_USERNAME", + value = var.db_username + }, + { + name = "DATABASE_HOST", + value = var.db_host + }, + { + name = "DATABASE_PORT", + value = tostring(var.db_port), + }, + { + name = "SIP_PORT", + value = tostring(var.sip_port) + } + ] + }, + { + name = "opensips_scheduler", + image = "${var.scheduler_image}:latest", + essential = true, + mountPoints = [ + { + sourceVolume = "opensips", + containerPath = "/var/opensips" + } + ], + environment = [ + { + name = "FIFO_NAME", + value = var.opensips_fifo_name + }, + { + name = "MI_COMMANDS", + value = "lb_reload,domain_reload,rtpengine_reload" + } + ] + } + ]) + + memory = module.container_instances.ec2_instance_type.memory_size - 512 + + volume { + name = "opensips" + } +} + +resource "aws_ecs_service" "this" { + name = aws_ecs_task_definition.this.family + cluster = var.ecs_cluster.id + task_definition = aws_ecs_task_definition.this.arn + desired_count = var.min_tasks + deployment_minimum_healthy_percent = 50 + deployment_maximum_percent = 100 + + capacity_provider_strategy { + capacity_provider = aws_ecs_capacity_provider.this.name + weight = 1 + } + + placement_constraints { + type = "distinctInstance" + } + + depends_on = [ + aws_iam_role.task_execution_role + ] + + lifecycle { + ignore_changes = [task_definition, desired_count] + } +} diff --git a/infrastructure/modules/client_gateway/eip.tf b/infrastructure/modules/client_gateway/eip.tf new file mode 100644 index 000000000..1a86d25ec --- /dev/null +++ b/infrastructure/modules/client_gateway/eip.tf @@ -0,0 +1,10 @@ +resource "aws_eip" "client_gateway" { + count = var.assign_eips ? var.max_tasks : 0 + domain = "vpc" + + tags = { + Name = "${var.identifier} ${count.index + 1}" + (var.identifier) = "true" + Priority = count.index + 1 + } +} diff --git a/infrastructure/modules/client_gateway/iam.tf b/infrastructure/modules/client_gateway/iam.tf new file mode 100644 index 000000000..1d3cb798b --- /dev/null +++ b/infrastructure/modules/client_gateway/iam.tf @@ -0,0 +1,76 @@ +resource "aws_iam_policy" "container_instance_custom_policy" { + name = "${var.identifier}-container-instance-custom_policy" + + policy = < eip } + + reference_name = "${var.subdomain}-${each.key + 1}" + ip_address = each.value.public_ip + port = var.sip_port + type = "TCP" + request_interval = 30 + + tags = { + Name = "${var.subdomain}-${each.key + 1}" + } +} + +resource "aws_route53_record" "client_gateway" { + for_each = aws_route53_health_check.client_gateway + zone_id = var.route53_zone.zone_id + name = var.subdomain + type = "A" + ttl = 300 + records = [each.value.ip_address] + + multivalue_answer_routing_policy = true + set_identifier = "${var.identifier}-${each.key + 1}" + health_check_id = each.value.id +} + +resource "aws_lambda_invocation" "create_domain" { + for_each = aws_route53_record.client_gateway + function_name = var.services_function.this.function_name + + input = jsonencode({ + serviceAction = "CreateDomain", + parameters = { + domain = each.value.fqdn + } + }) +} diff --git a/infrastructure/modules/client_gateway/sg.tf b/infrastructure/modules/client_gateway/sg.tf new file mode 100644 index 000000000..f658b47ca --- /dev/null +++ b/infrastructure/modules/client_gateway/sg.tf @@ -0,0 +1,26 @@ +resource "aws_security_group_rule" "healthcheck" { + type = "ingress" + to_port = var.sip_port + protocol = "tcp" + from_port = var.sip_port + security_group_id = module.container_instances.security_group.id + cidr_blocks = data.aws_ip_ranges.route53_healthchecks.cidr_blocks +} + +resource "aws_security_group_rule" "sip" { + type = "ingress" + to_port = var.sip_port + protocol = "udp" + from_port = var.sip_port + security_group_id = module.container_instances.security_group.id + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "icmp" { + type = "ingress" + to_port = -1 + protocol = "icmp" + from_port = -1 + security_group_id = module.container_instances.security_group.id + cidr_blocks = ["0.0.0.0/0"] +} diff --git a/infrastructure/modules/somleng_switch/templates/assign_eip.sh b/infrastructure/modules/client_gateway/templates/assign_eip.sh similarity index 100% rename from infrastructure/modules/somleng_switch/templates/assign_eip.sh rename to infrastructure/modules/client_gateway/templates/assign_eip.sh diff --git a/infrastructure/modules/client_gateway/variables.tf b/infrastructure/modules/client_gateway/variables.tf new file mode 100644 index 000000000..0fdeb0a23 --- /dev/null +++ b/infrastructure/modules/client_gateway/variables.tf @@ -0,0 +1,33 @@ +variable "identifier" {} +variable "app_environment" {} +variable "aws_region" {} +variable "vpc" {} +variable "ecs_cluster" {} +variable "app_image" {} +variable "scheduler_image" {} +variable "sip_port" {} +variable "subdomain" {} +variable "route53_zone" {} +variable "db_security_group" {} +variable "db_password_parameter" {} +variable "db_name" {} +variable "db_username" {} +variable "db_host" {} +variable "db_port" {} +variable "services_function" {} + +variable "opensips_fifo_name" { + default = "/var/opensips/opensips_fifo" +} + +variable "max_tasks" { + default = 2 +} + +variable "min_tasks" { + default = 2 +} + +variable "assign_eips" { + default = true +} diff --git a/infrastructure/modules/container_instances/asg.tf b/infrastructure/modules/container_instances/asg.tf new file mode 100644 index 000000000..6612ee987 --- /dev/null +++ b/infrastructure/modules/container_instances/asg.tf @@ -0,0 +1,32 @@ +resource "aws_autoscaling_group" "this" { + name = var.app_identifier + + launch_template { + id = aws_launch_template.this.id + version = aws_launch_template.this.latest_version + } + + vpc_zone_identifier = var.instance_subnets + max_size = var.max_capacity + min_size = 0 + desired_capacity = 0 + wait_for_capacity_timeout = 0 + protect_from_scale_in = true + + tag { + key = "Name" + value = var.app_identifier + propagate_at_launch = true + } + + tag { + key = "AmazonECSManaged" + value = "" + propagate_at_launch = true + } + + lifecycle { + ignore_changes = [desired_capacity] + create_before_destroy = true + } +} diff --git a/infrastructure/modules/container_instances/iam.tf b/infrastructure/modules/container_instances/iam.tf new file mode 100644 index 000000000..b12969284 --- /dev/null +++ b/infrastructure/modules/container_instances/iam.tf @@ -0,0 +1,52 @@ +locals { + create_iam_role = var.iam_instance_profile == null + iam_instance_profile = local.create_iam_role ? aws_iam_instance_profile.this[0] : var.iam_instance_profile +} + +data "aws_iam_role" "this" { + name = local.iam_instance_profile.role +} + +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "this" { + count = local.create_iam_role ? 1 : 0 + name = "${var.app_identifier}_ecs_container_instance_role" + + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +resource "aws_iam_instance_profile" "this" { + count = local.create_iam_role ? 1 : 0 + name = "${var.app_identifier}_ecs_container_instance_profile" + role = aws_iam_role.this[0].name +} + +resource "aws_iam_role_policy_attachment" "ecs" { + count = local.create_iam_role ? 1 : 0 + role = aws_iam_role.this[0].id + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role_policy_attachment" "ecs_ec2_role" { + count = local.create_iam_role ? 1 : 0 + role = aws_iam_role.this[0].id + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" +} + +resource "aws_iam_role_policy_attachment" "ssm" { + count = local.create_iam_role ? 1 : 0 + role = aws_iam_role.this[0].name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" +} diff --git a/infrastructure/modules/container_instances/launch_template.tf b/infrastructure/modules/container_instances/launch_template.tf new file mode 100644 index 000000000..7418514b2 --- /dev/null +++ b/infrastructure/modules/container_instances/launch_template.tf @@ -0,0 +1,56 @@ +locals { + user_data = concat(var.user_data, [ + { + path = "/opt/setup.sh" + content = templatefile( + "${path.module}/templates/setup.sh", + { + cluster_name = var.cluster_name + } + ) + permissions = "755" + } + ]) +} + +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html +# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/retrieve-ecs-optimized_AMI.html +data "aws_ssm_parameter" "amd64_ami" { + name = "/aws/service/ecs/optimized-ami/amazon-linux-2023/recommended" +} + +data "aws_ssm_parameter" "arm64_ami" { + name = "/aws/service/ecs/optimized-ami/amazon-linux-2023/arm64/recommended" +} + +data "aws_ec2_instance_type" "this" { + instance_type = var.instance_type +} + +resource "aws_launch_template" "this" { + name_prefix = var.app_identifier + image_id = jsondecode((var.architecture == "arm64" ? data.aws_ssm_parameter.arm64_ami : data.aws_ssm_parameter.amd64_ami).value).image_id + instance_type = data.aws_ec2_instance_type.this.instance_type + + iam_instance_profile { + name = local.iam_instance_profile.name + } + + network_interfaces { + associate_public_ip_address = var.associate_public_ip_address + security_groups = concat([aws_security_group.this.id], var.security_groups) + } + + user_data = base64encode(join("\n", [ + "#cloud-config", + yamlencode({ + # https://cloudinit.readthedocs.io/en/latest/topics/modules.html + write_files : local.user_data, + runcmd : [for i, v in local.user_data : v.path] + }) + ])) + + lifecycle { + create_before_destroy = true + } +} diff --git a/infrastructure/modules/container_instances/main.tf b/infrastructure/modules/container_instances/main.tf deleted file mode 100644 index 813b32d3c..000000000 --- a/infrastructure/modules/container_instances/main.tf +++ /dev/null @@ -1,164 +0,0 @@ -# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html -# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/retrieve-ecs-optimized_AMI.html -data "aws_ssm_parameter" "amd64_ami" { - name = "/aws/service/ecs/optimized-ami/amazon-linux-2023/recommended" -} - -data "aws_ssm_parameter" "arm64_ami" { - name = "/aws/service/ecs/optimized-ami/amazon-linux-2023/arm64/recommended" -} - -data "aws_ec2_instance_type" "this" { - instance_type = var.instance_type -} - -locals { - user_data = concat(var.user_data, [ - { - path = "/opt/setup.sh" - content = templatefile( - "${path.module}/templates/setup.sh", - { - cluster_name = var.cluster_name - } - ) - permissions = "755" - } - ]) -} - -# IAM - -resource "aws_iam_role" "this" { - name = "${var.app_identifier}_ecs_container_instance_role" - - assume_role_policy = < 0 ? 1 : 0 + name = var.identifier + service_namespace = aws_appautoscaling_target.public_gateway_scale_target[count.index].service_namespace + resource_id = aws_appautoscaling_target.public_gateway_scale_target[count.index].resource_id + scalable_dimension = aws_appautoscaling_target.public_gateway_scale_target[count.index].scalable_dimension + policy_type = "TargetTrackingScaling" + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "ECSServiceAverageCPUUtilization" + } + + target_value = 30 + scale_in_cooldown = 300 + scale_out_cooldown = 60 + } +} + +resource "aws_appautoscaling_target" "public_gateway_scale_target" { + count = var.min_tasks > 0 ? 1 : 0 + service_namespace = "ecs" + resource_id = "service/${var.ecs_cluster.name}/${aws_ecs_service.public_gateway[count.index].name}" + scalable_dimension = "ecs:service:DesiredCount" + min_capacity = var.min_tasks + max_capacity = var.max_tasks +} diff --git a/infrastructure/modules/public_gateway/cloudwatch.tf b/infrastructure/modules/public_gateway/cloudwatch.tf new file mode 100644 index 000000000..4c1f177d2 --- /dev/null +++ b/infrastructure/modules/public_gateway/cloudwatch.tf @@ -0,0 +1,4 @@ +resource "aws_cloudwatch_log_group" "this" { + name = var.identifier + retention_in_days = 7 +} diff --git a/infrastructure/modules/public_gateway/container_instances.tf b/infrastructure/modules/public_gateway/container_instances.tf new file mode 100644 index 000000000..237891eb8 --- /dev/null +++ b/infrastructure/modules/public_gateway/container_instances.tf @@ -0,0 +1,9 @@ +module "container_instances" { + source = "../container_instances" + + app_identifier = var.identifier + vpc = var.vpc + instance_subnets = var.vpc.private_subnets + max_capacity = var.max_tasks * 2 + cluster_name = var.ecs_cluster.name +} diff --git a/infrastructure/modules/public_gateway/ecs.tf b/infrastructure/modules/public_gateway/ecs.tf new file mode 100644 index 000000000..33a51b770 --- /dev/null +++ b/infrastructure/modules/public_gateway/ecs.tf @@ -0,0 +1,174 @@ +resource "aws_ecs_capacity_provider" "this" { + name = var.identifier + + auto_scaling_group_provider { + auto_scaling_group_arn = module.container_instances.autoscaling_group.arn + managed_termination_protection = "ENABLED" + managed_draining = "ENABLED" + + managed_scaling { + maximum_scaling_step_size = 1000 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 100 + } + } +} + +resource "aws_ecs_task_definition" "this" { + family = var.identifier + network_mode = "awsvpc" + requires_compatibilities = ["EC2"] + task_role_arn = aws_iam_role.task_role.arn + execution_role_arn = aws_iam_role.task_execution_role.arn + container_definitions = jsonencode([ + { + name = "public_gateway", + image = "${var.app_image}:latest", + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.this.name, + awslogs-region = var.aws_region, + awslogs-stream-prefix = var.app_environment + } + }, + essential = true, + portMappings = [ + { + containerPort = var.sip_port, + protocol = "udp" + }, + { + containerPort = var.sip_alternative_port, + protocol = "udp" + } + ], + mountPoints = [ + { + sourceVolume = "opensips", + containerPath = "/var/opensips" + } + ], + healthCheck = { + command = ["CMD-SHELL", "nc -z -w 5 $(hostname -i) $SIP_PORT"], + interval = 10, + retries = 10, + timeout = 5 + }, + secrets = [ + { + name = "DATABASE_PASSWORD", + valueFrom = var.db_password_parameter.arn + } + ], + environment = [ + { + name = "FIFO_NAME", + value = var.opensips_fifo_name + }, + { + name = "DATABASE_NAME", + value = var.db_name + }, + { + name = "DATABASE_USERNAME", + value = var.db_username + }, + { + name = "DATABASE_HOST", + value = var.db_host + }, + { + name = "DATABASE_PORT", + value = tostring(var.db_port) + }, + { + name = "SIP_PORT", + value = tostring(var.sip_port) + }, + { + name = "SIP_ALTERNATIVE_PORT", + value = tostring(var.sip_alternative_port) + }, + { + name = "SIP_ADVERTISED_IP", + value = tostring(var.global_accelerator.ip_sets[0].ip_addresses[0]) + } + ] + }, + { + name = "opensips_scheduler", + image = "${var.scheduler_image}:latest", + essential = true, + mountPoints = [ + { + sourceVolume = "opensips", + containerPath = "/var/opensips" + } + ], + environment = [ + { + name = "FIFO_NAME", + value = var.opensips_fifo_name + }, + { + name = "MI_COMMANDS", + value = "lb_reload,address_reload" + } + ] + } + ]) + + memory = max((module.container_instances.ec2_instance_type.memory_size - 512), 128) + + volume { + name = "opensips" + } +} + +resource "aws_ecs_service" "public_gateway" { + count = var.min_tasks > 0 ? 1 : 0 + name = aws_ecs_task_definition.this.family + cluster = var.ecs_cluster.id + task_definition = aws_ecs_task_definition.this.arn + desired_count = var.min_tasks + + network_configuration { + subnets = var.vpc.private_subnets + security_groups = [ + aws_security_group.this.id, + var.db_security_group.id + ] + } + + load_balancer { + target_group_arn = aws_lb_target_group.sip.arn + container_name = "public_gateway" + container_port = var.sip_port + } + + load_balancer { + target_group_arn = aws_lb_target_group.sip_alternative.arn + container_name = "public_gateway" + container_port = var.sip_alternative_port + } + + capacity_provider_strategy { + capacity_provider = aws_ecs_capacity_provider.this.name + weight = 1 + } + + deployment_circuit_breaker { + enable = true + rollback = true + } + + depends_on = [ + aws_iam_role.task_role + ] + + lifecycle { + ignore_changes = [task_definition, desired_count] + } +} diff --git a/infrastructure/modules/public_gateway/global_accelerator.tf b/infrastructure/modules/public_gateway/global_accelerator.tf new file mode 100644 index 000000000..29f414b15 --- /dev/null +++ b/infrastructure/modules/public_gateway/global_accelerator.tf @@ -0,0 +1,24 @@ +resource "aws_globalaccelerator_listener" "this" { + accelerator_arn = var.global_accelerator.id + protocol = "UDP" + + port_range { + from_port = var.sip_port + to_port = var.sip_port + } + + port_range { + from_port = var.sip_alternative_port + to_port = var.sip_alternative_port + } +} + +resource "aws_globalaccelerator_endpoint_group" "public_gateway" { + count = var.min_tasks > 0 ? 1 : 0 + listener_arn = aws_globalaccelerator_listener.this.id + + endpoint_configuration { + endpoint_id = aws_lb.public_gateway_nlb[count.index].arn + client_ip_preservation_enabled = true + } +} diff --git a/infrastructure/modules/public_gateway/iam.tf b/infrastructure/modules/public_gateway/iam.tf new file mode 100644 index 000000000..1c4780aaa --- /dev/null +++ b/infrastructure/modules/public_gateway/iam.tf @@ -0,0 +1,68 @@ +resource "aws_iam_role" "task_role" { + name = "${var.identifier}-ecsTaskRole" + + assume_role_policy = < 0 ? length(var.vpc.public_subnets) : 0 + domain = "vpc" + + tags = { + Name = "Public Gateway NLB IP" + } +} + +resource "aws_lb" "public_gateway_nlb" { + count = var.min_tasks > 0 ? 1 : 0 + name = var.identifier + load_balancer_type = "network" + enable_cross_zone_load_balancing = true + + security_groups = [aws_security_group.nlb.id] + + access_logs { + bucket = var.logs_bucket.id + prefix = var.identifier + enabled = true + } + + dynamic "subnet_mapping" { + for_each = var.vpc.public_subnets + content { + subnet_id = subnet_mapping.value + allocation_id = aws_eip.public_gateway_nlb.*.id[subnet_mapping.key] + } + } +} + +# Target Groups + +resource "aws_lb_target_group" "sip" { + name = "${var.identifier}-sip" + port = var.sip_port + protocol = "UDP" + target_type = "ip" + vpc_id = var.vpc.vpc_id + + connection_termination = true + + health_check { + protocol = "TCP" + port = var.sip_port + healthy_threshold = 3 + interval = 10 + } +} + +resource "aws_lb_listener" "sip" { + count = var.min_tasks > 0 ? 1 : 0 + load_balancer_arn = aws_lb.public_gateway_nlb[count.index].arn + port = var.sip_port + protocol = "UDP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.sip.arn + } +} + +resource "aws_lb_target_group" "sip_alternative" { + name = "${var.identifier}-sip-alt" + port = var.sip_alternative_port + protocol = "UDP" + target_type = "ip" + vpc_id = var.vpc.vpc_id + + connection_termination = true + + health_check { + protocol = "TCP" + port = var.sip_port + healthy_threshold = 3 + interval = 10 + } +} + +resource "aws_lb_listener" "sip_alternative" { + count = var.min_tasks > 0 ? 1 : 0 + load_balancer_arn = aws_lb.public_gateway_nlb[count.index].arn + port = var.sip_alternative_port + protocol = "UDP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.sip_alternative.arn + } +} diff --git a/infrastructure/modules/public_gateway/outputs.tf b/infrastructure/modules/public_gateway/outputs.tf new file mode 100644 index 000000000..3f03ede7f --- /dev/null +++ b/infrastructure/modules/public_gateway/outputs.tf @@ -0,0 +1,3 @@ +output "capacity_provider" { + value = aws_ecs_capacity_provider.this +} diff --git a/infrastructure/modules/public_gateway/sg.tf b/infrastructure/modules/public_gateway/sg.tf new file mode 100644 index 000000000..28d4a0e52 --- /dev/null +++ b/infrastructure/modules/public_gateway/sg.tf @@ -0,0 +1,40 @@ +resource "aws_security_group" "this" { + name = var.identifier + vpc_id = var.vpc.vpc_id +} + +resource "aws_security_group_rule" "healthcheck" { + type = "ingress" + to_port = var.sip_port + protocol = "tcp" + from_port = var.sip_port + security_group_id = aws_security_group.this.id + cidr_blocks = [var.vpc.vpc_cidr_block] +} + +resource "aws_security_group_rule" "sip" { + type = "ingress" + to_port = var.sip_port + protocol = "udp" + from_port = var.sip_port + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "sip_alternative" { + type = "ingress" + to_port = var.sip_alternative_port + protocol = "udp" + from_port = var.sip_alternative_port + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "egress" { + type = "egress" + to_port = 0 + protocol = "-1" + from_port = 0 + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} diff --git a/infrastructure/modules/public_gateway/variables.tf b/infrastructure/modules/public_gateway/variables.tf new file mode 100644 index 000000000..160ba271c --- /dev/null +++ b/infrastructure/modules/public_gateway/variables.tf @@ -0,0 +1,29 @@ +variable "identifier" {} +variable "app_environment" {} +variable "aws_region" {} +variable "vpc" {} +variable "ecs_cluster" {} +variable "app_image" {} +variable "scheduler_image" {} +variable "sip_port" {} +variable "sip_alternative_port" {} +variable "db_security_group" {} +variable "db_password_parameter" {} +variable "db_name" {} +variable "db_username" {} +variable "db_host" {} +variable "db_port" {} +variable "global_accelerator" {} +variable "logs_bucket" {} + +variable "opensips_fifo_name" { + default = "/var/opensips/opensips_fifo" +} + +variable "max_tasks" { + default = 4 +} + +variable "min_tasks" { + default = 1 +} diff --git a/infrastructure/modules/s3_bucket/iam.tf b/infrastructure/modules/s3_bucket/iam.tf new file mode 100644 index 000000000..b1a6cf9f7 --- /dev/null +++ b/infrastructure/modules/s3_bucket/iam.tf @@ -0,0 +1,28 @@ +resource "aws_iam_user" "this" { + name = var.iam_username + +} + +resource "aws_iam_access_key" "this" { + user = aws_iam_user.this.name +} + +resource "aws_iam_user_policy" "this" { + name = aws_iam_user.this.name + user = aws_iam_user.this.name + + policy = < eip } - - reference_name = "${var.client_gateway_subdomain}-${each.key + 1}" - ip_address = each.value.public_ip - port = var.sip_port - type = "TCP" - request_interval = 30 - - tags = { - Name = "${var.client_gateway_subdomain}-${each.key + 1}" - } -} - -resource "aws_route53_record" "client_gateway" { - for_each = aws_route53_health_check.client_gateway - zone_id = var.route53_zone.zone_id - name = var.client_gateway_subdomain - type = "A" - ttl = 300 - records = [each.value.ip_address] - - multivalue_answer_routing_policy = true - set_identifier = "${var.client_gateway_identifier}-${each.key + 1}" - health_check_id = each.value.id -} - -resource "aws_lambda_invocation" "create_domain" { - for_each = aws_route53_record.client_gateway - function_name = aws_lambda_function.services.function_name - - input = jsonencode({ - serviceAction = "CreateDomain", - parameters = { - domain = each.value.fqdn - } - }) -} diff --git a/infrastructure/modules/somleng_switch/docker.tf b/infrastructure/modules/somleng_switch/docker.tf deleted file mode 100644 index 982ebc410..000000000 --- a/infrastructure/modules/somleng_switch/docker.tf +++ /dev/null @@ -1,15 +0,0 @@ -data "aws_ecr_authorization_token" "token" {} - -provider "docker" { - registry_auth { - address = split("/", var.s3_mpeg_ecr_repository_url)[0] - username = data.aws_ecr_authorization_token.token.user_name - password = data.aws_ecr_authorization_token.token.password - } - - registry_auth { - address = split("/", var.services_ecr_repository_url)[0] - username = data.aws_ecr_authorization_token.token.user_name - password = data.aws_ecr_authorization_token.token.password - } -} diff --git a/infrastructure/modules/somleng_switch/ecs.tf b/infrastructure/modules/somleng_switch/ecs.tf deleted file mode 100644 index 61b24de79..000000000 --- a/infrastructure/modules/somleng_switch/ecs.tf +++ /dev/null @@ -1,19 +0,0 @@ -resource "aws_ecs_cluster" "cluster" { - name = var.cluster_name - - setting { - name = "containerInsights" - value = var.container_insights_enabled ? "enabled" : "disabled" - } -} - -resource "aws_ecs_cluster_capacity_providers" "cluster" { - cluster_name = aws_ecs_cluster.cluster.name - - capacity_providers = [ - aws_ecs_capacity_provider.switch.name, - aws_ecs_capacity_provider.public_gateway.name, - aws_ecs_capacity_provider.client_gateway.name, - aws_ecs_capacity_provider.media_proxy.name - ] -} diff --git a/infrastructure/modules/somleng_switch/ecs_cwagent_daemon_service.tf b/infrastructure/modules/somleng_switch/ecs_cwagent_daemon_service.tf deleted file mode 100644 index b1c2d41f1..000000000 --- a/infrastructure/modules/somleng_switch/ecs_cwagent_daemon_service.tf +++ /dev/null @@ -1,163 +0,0 @@ -# https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/deploy-container-insights-ECS-instancelevel.html#deploy-container-insights-ECS-instancelevel-manual - -# IAM Roles -# https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/deploy-container-insights-ECS-instancelevel.html#deploy-container-insights-ECS-instancelevel-IAMRoles -resource "aws_iam_role" "ecs_cwagent_daemon_service_task_role" { - name = "${var.switch_identifier}-CWAgentECSTaskRole" - - assume_role_policy = < 0 ? 1 : 0 - listener_arn = aws_globalaccelerator_listener.public_gateway.id - - endpoint_configuration { - endpoint_id = aws_lb.public_gateway_nlb[count.index].arn - client_ip_preservation_enabled = true - } -} - -# IAM -resource "aws_iam_role" "public_gateway_task_role" { - name = "${var.public_gateway_identifier}-ecsTaskRole" - - assume_role_policy = < 0 ? 1 : 0 - name = aws_ecs_task_definition.public_gateway.family - cluster = aws_ecs_cluster.cluster.id - task_definition = aws_ecs_task_definition.public_gateway.arn - desired_count = var.public_gateway_min_tasks - - network_configuration { - subnets = var.vpc.private_subnets - security_groups = [ - aws_security_group.public_gateway.id, - var.db_security_group - ] - } - - load_balancer { - target_group_arn = aws_lb_target_group.sip.arn - container_name = "public_gateway" - container_port = var.sip_port - } - - load_balancer { - target_group_arn = aws_lb_target_group.sip_alternative.arn - container_name = "public_gateway" - container_port = var.sip_alternative_port - } - - capacity_provider_strategy { - capacity_provider = aws_ecs_capacity_provider.public_gateway.name - weight = 1 - } - - depends_on = [ - aws_iam_role.public_gateway_task_role - ] - - lifecycle { - ignore_changes = [task_definition, desired_count] - } -} - -# Load Balancer - -resource "aws_security_group" "public_gateway_nlb" { - name = "${var.public_gateway_identifier}-nlb" - vpc_id = var.vpc.vpc_id -} - -resource "aws_security_group_rule" "public_gateway_nlb_sip_ingress" { - type = "ingress" - from_port = var.sip_port - to_port = var.sip_port - protocol = "udp" - cidr_blocks = ["0.0.0.0/0"] - - security_group_id = aws_security_group.public_gateway_nlb.id -} - -resource "aws_security_group_rule" "public_gateway_nlb_sip_alternative_ingress" { - type = "ingress" - from_port = var.sip_alternative_port - to_port = var.sip_alternative_port - protocol = "udp" - cidr_blocks = ["0.0.0.0/0"] - - security_group_id = aws_security_group.public_gateway_nlb.id -} - -resource "aws_security_group_rule" "public_gateway_nlb_udp_egress" { - type = "egress" - from_port = 0 - to_port = 65535 - protocol = "udp" - cidr_blocks = ["0.0.0.0/0"] - - security_group_id = aws_security_group.public_gateway_nlb.id -} - -resource "aws_security_group_rule" "public_gateway_nlb_tcp_egress" { - type = "egress" - from_port = 0 - to_port = 65535 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - - security_group_id = aws_security_group.public_gateway_nlb.id -} - -resource "aws_eip" "public_gateway_nlb" { - count = var.public_gateway_min_tasks > 0 ? length(var.vpc.public_subnets) : 0 - domain = "vpc" - - tags = { - Name = "Public Gateway NLB IP" - } -} - -resource "aws_lb" "public_gateway_nlb" { - count = var.public_gateway_min_tasks > 0 ? 1 : 0 - name = var.public_gateway_identifier - load_balancer_type = "network" - enable_cross_zone_load_balancing = true - - security_groups = [aws_security_group.public_gateway_nlb.id] - - access_logs { - bucket = var.logs_bucket.id - prefix = var.public_gateway_identifier - enabled = true - } - - dynamic "subnet_mapping" { - for_each = var.vpc.public_subnets - content { - subnet_id = subnet_mapping.value - allocation_id = aws_eip.public_gateway_nlb.*.id[subnet_mapping.key] - } - } -} - -# Target Groups - -resource "aws_lb_target_group" "sip" { - name = "${var.public_gateway_identifier}-sip" - port = var.sip_port - protocol = "UDP" - target_type = "ip" - vpc_id = var.vpc.vpc_id - - connection_termination = true - - health_check { - protocol = "TCP" - port = var.sip_port - healthy_threshold = 3 - interval = 10 - } -} - -resource "aws_lb_listener" "sip" { - count = var.public_gateway_min_tasks > 0 ? 1 : 0 - load_balancer_arn = aws_lb.public_gateway_nlb[count.index].arn - port = var.sip_port - protocol = "UDP" - - default_action { - type = "forward" - target_group_arn = aws_lb_target_group.sip.arn - } -} - -resource "aws_lb_target_group" "sip_alternative" { - name = "${var.public_gateway_identifier}-sip-alt" - port = var.sip_alternative_port - protocol = "UDP" - target_type = "ip" - vpc_id = var.vpc.vpc_id - - connection_termination = true - - health_check { - protocol = "TCP" - port = var.sip_port - healthy_threshold = 3 - interval = 10 - } -} - -resource "aws_lb_listener" "sip_alternative" { - count = var.public_gateway_min_tasks > 0 ? 1 : 0 - load_balancer_arn = aws_lb.public_gateway_nlb[count.index].arn - port = var.sip_alternative_port - protocol = "UDP" - - default_action { - type = "forward" - target_group_arn = aws_lb_target_group.sip_alternative.arn - } -} - -# Autoscaling -resource "aws_appautoscaling_policy" "public_gateway_policy" { - count = var.public_gateway_min_tasks > 0 ? 1 : 0 - name = var.public_gateway_identifier - service_namespace = aws_appautoscaling_target.public_gateway_scale_target[count.index].service_namespace - resource_id = aws_appautoscaling_target.public_gateway_scale_target[count.index].resource_id - scalable_dimension = aws_appautoscaling_target.public_gateway_scale_target[count.index].scalable_dimension - policy_type = "TargetTrackingScaling" - - target_tracking_scaling_policy_configuration { - predefined_metric_specification { - predefined_metric_type = "ECSServiceAverageCPUUtilization" - } - - target_value = 30 - scale_in_cooldown = 300 - scale_out_cooldown = 60 - } -} - -resource "aws_appautoscaling_target" "public_gateway_scale_target" { - count = var.public_gateway_min_tasks > 0 ? 1 : 0 - service_namespace = "ecs" - resource_id = "service/${aws_ecs_cluster.cluster.name}/${aws_ecs_service.public_gateway[count.index].name}" - scalable_dimension = "ecs:service:DesiredCount" - min_capacity = var.public_gateway_min_tasks - max_capacity = var.public_gateway_max_tasks -} diff --git a/infrastructure/modules/somleng_switch/s3_mpeg.tf b/infrastructure/modules/somleng_switch/s3_mpeg.tf deleted file mode 100644 index 352f8ca09..000000000 --- a/infrastructure/modules/somleng_switch/s3_mpeg.tf +++ /dev/null @@ -1,113 +0,0 @@ -locals { - s3_mpeg_function_name = "${var.s3_mpeg_identifier}" - s3_filter_suffix = ".wav" -} - -resource "docker_image" "s3_mpeg" { - name = "${var.s3_mpeg_ecr_repository_url}:latest" - build { - context = abspath("${path.module}/../../../components/s3_mpeg") - } -} - -resource "docker_registry_image" "s3_mpeg" { - name = docker_image.s3_mpeg.name - keep_remotely = true -} - -resource "aws_iam_role" "s3_mpeg" { - name = local.s3_mpeg_function_name - - assume_role_policy = <&1 | grep '200 OK' > /dev/null"], - interval = 10, - retries = 10, - timeout = 5 - }, - secrets = [ - { - name = "APP_MASTER_KEY", - valueFrom = aws_ssm_parameter.switch_application_master_key.arn - }, - { - name = "AHN_CORE_PASSWORD", - valueFrom = aws_ssm_parameter.rayo_password.arn - } - ], - environment = [ - { - name = "AHN_ENV", - value = var.app_environment - }, - { - name = "APP_ENV", - value = var.app_environment - }, - { - name = "RACK_ENV", - value = var.app_environment - }, - { - name = "AWS_DEFAULT_REGION", - value = var.aws_region - }, - { - name = "AHN_CORE_HTTP_PORT", - value = tostring(var.switch_appserver_port) - }, - { - name = "AHN_CORE_PORT", - value = tostring(var.rayo_port) - }, - { - name = "SERVICES_FUNCTION_ARN", - value = aws_lambda_function.services.arn - }, - { - name = "REDIS_URL", - value = "redis://localhost:${var.redis_port}/1" - } - ] - }, - { - name = "freeswitch", - image = "${var.freeswitch_image}:latest", - logConfiguration = { - logDriver = "awslogs", - options = { - awslogs-group = aws_cloudwatch_log_group.freeswitch.name, - awslogs-region = var.aws_region, - awslogs-stream-prefix = var.app_environment - } - }, - startTimeout = 120, - healthCheck = { - command = [ - "CMD-SHELL", - "fs_cli -p $FS_EVENT_SOCKET_PASSWORD -x 'rayo status' | rayo_status_parser" - ] - interval = 10, - retries = 10 - timeout = 5 - } - essential = true, - portMappings = [ - { - containerPort = var.rayo_port, - protocol = "tcp" - }, - { - containerPort = var.sip_port, - protocol = "udp" - }, - { - containerPort = var.sip_alternative_port, - protocol = "udp" - }, - { - containerPort = var.freeswitch_event_socket_port, - protocol = "tcp" - } - ], - mountPoints = [ - { - containerPath = local.cache_directory, - sourceVolume = local.efs_volume_name - } - ], - secrets = [ - { - name = "FS_MOD_RAYO_PASSWORD", - valueFrom = aws_ssm_parameter.rayo_password.arn - }, - { - name = "FS_MOD_JSON_CDR_PASSWORD", - valueFrom = var.json_cdr_password_parameter_arn - }, - { - name = "FS_RECORDINGS_BUCKET_ACCESS_KEY_ID", - valueFrom = aws_ssm_parameter.recordings_bucket_access_key_id.arn - }, - { - name = "FS_RECORDINGS_BUCKET_SECRET_ACCESS_KEY", - valueFrom = aws_ssm_parameter.recordings_bucket_secret_access_key.arn - }, - { - name = "FS_EVENT_SOCKET_PASSWORD", - valueFrom = aws_ssm_parameter.freeswitch_event_socket_password.arn - } - ], - environment = [ - { - name = "AWS_DEFAULT_REGION", - value = var.aws_region - }, - { - name = "FS_CACHE_DIRECTORY", - value = local.cache_directory - }, - { - name = "FS_STORAGE_DIRECTORY", - value = "${local.cache_directory}/freeswitch/storage" - }, - { - name = "FS_TTS_CACHE_DIRECTORY", - value = "${local.cache_directory}/freeswitch/tts_cache" - }, - { - name = "FS_LOG_DIRECTORY", - value = "${local.cache_directory}/freeswitch/logs" - }, - { - name = "FS_EXTERNAL_RTP_IP", - value = var.external_rtp_ip - }, - { - name = "FS_ALTERNATIVE_SIP_OUTBOUND_IP", - value = var.alternative_sip_outbound_ip - }, - { - name = "FS_ALTERNATIVE_RTP_IP", - value = var.alternative_rtp_ip - }, - { - name = "FS_MOD_RAYO_PORT", - value = tostring(var.rayo_port) - }, - { - name = "FS_MOD_JSON_CDR_URL", - value = var.json_cdr_url - }, - { - name = "FS_RECORDINGS_BUCKET_NAME", - value = aws_s3_bucket.recordings.id - }, - { - name = "FS_RECORDINGS_BUCKET_REGION", - value = aws_s3_bucket.recordings.region - }, - { - name = "FS_EVENT_SOCKET_PORT", - value = tostring(var.freeswitch_event_socket_port) - }, - { - name = "FS_SIP_PORT", - value = tostring(var.sip_port) - }, - { - name = "FS_SIP_ALTERNATIVE_PORT", - value = tostring(var.sip_alternative_port) - } - ] - }, - { - name = "redis", - image = "public.ecr.aws/docker/library/redis:alpine", - logConfiguration = { - logDriver = "awslogs", - options = { - awslogs-group = aws_cloudwatch_log_group.redis.name, - awslogs-region = var.aws_region, - awslogs-stream-prefix = var.app_environment - } - }, - essential = true, - healthCheck = { - command = ["CMD-SHELL", "redis-cli", "--raw", "incr", "ping"], - interval = 10, - retries = 10, - timeout = 5 - }, - portMappings = [ - { - containerPort = var.redis_port - } - ] - }, - { - name = "freeswitch-event-logger", - image = "${var.freeswitch_event_logger_image}:latest", - logConfiguration = { - logDriver = "awslogs", - options = { - awslogs-group = aws_cloudwatch_log_group.freeswitch_event_logger.name, - awslogs-region = var.aws_region, - awslogs-stream-prefix = var.app_environment - } - }, - startTimeout = 120, - essential = true, - secrets = [ - { - name = "EVENT_SOCKET_PASSWORD", - valueFrom = aws_ssm_parameter.freeswitch_event_socket_password.arn - } - ], - dependsOn = [ - { - containerName = "freeswitch", - condition = "HEALTHY" - }, - { - containerName = "redis", - condition = "HEALTHY" - } - ], - environment = [ - { - name = "EVENT_SOCKET_HOST", - value = "localhost:${var.freeswitch_event_socket_port}" - }, - { - name = "REDIS_URL", - value = "redis://localhost:${var.redis_port}/1" - } - ] - } - ]) - - task_role_arn = aws_iam_role.ecs_task_role.arn - execution_role_arn = aws_iam_role.task_execution_role.arn - memory = module.switch_container_instances.ec2_instance_type.memory_size - 512 - - volume { - name = local.efs_volume_name - - efs_volume_configuration { - file_system_id = aws_efs_file_system.cache.id - transit_encryption = "ENABLED" - } - } -} - -resource "aws_ecs_service" "switch" { - name = var.switch_identifier - cluster = aws_ecs_cluster.cluster.id - task_definition = aws_ecs_task_definition.switch.arn - desired_count = var.switch_min_tasks - - network_configuration { - subnets = var.vpc.private_subnets - security_groups = [ - aws_security_group.switch.id - ] - } - - capacity_provider_strategy { - capacity_provider = aws_ecs_capacity_provider.switch.name - weight = 1 - } - - placement_constraints { - type = "distinctInstance" - } - - load_balancer { - target_group_arn = aws_lb_target_group.switch_http.arn - container_name = "nginx" - container_port = var.switch_webserver_port - } - - lifecycle { - ignore_changes = [task_definition, desired_count] - } - - depends_on = [ - aws_iam_role.ecs_task_role - ] -} - -# Load Balancer - -resource "aws_lb_target_group" "switch_http" { - name = "${var.switch_identifier}-internal" - port = var.switch_webserver_port - protocol = "HTTP" - vpc_id = var.vpc.vpc_id - target_type = "ip" - deregistration_delay = 60 - - health_check { - protocol = "HTTP" - path = "/health_checks" - healthy_threshold = 3 - interval = 10 - } -} - -resource "aws_lb_listener_rule" "switch_http" { - priority = var.app_environment == "production" ? 20 : 120 - - listener_arn = var.internal_listener.arn - - action { - type = "forward" - target_group_arn = aws_lb_target_group.switch_http.id - } - - condition { - host_header { - values = [aws_route53_record.switch.fqdn] - } - } - - lifecycle { - ignore_changes = [action] - } -} - - -# Autoscaling -resource "aws_appautoscaling_target" "switch_scale_target" { - service_namespace = "ecs" - resource_id = "service/${aws_ecs_cluster.cluster.name}/${aws_ecs_service.switch.name}" - scalable_dimension = "ecs:service:DesiredCount" - min_capacity = var.switch_min_tasks - max_capacity = var.switch_max_tasks -} - -resource "aws_appautoscaling_policy" "switch_policy" { - name = "switch-scale" - service_namespace = aws_appautoscaling_target.switch_scale_target.service_namespace - resource_id = aws_appautoscaling_target.switch_scale_target.resource_id - scalable_dimension = aws_appautoscaling_target.switch_scale_target.scalable_dimension - policy_type = "TargetTrackingScaling" - - target_tracking_scaling_policy_configuration { - predefined_metric_specification { - predefined_metric_type = "ECSServiceAverageCPUUtilization" - } - - target_value = 30 - scale_in_cooldown = 300 - scale_out_cooldown = 60 - } -} - -resource "aws_appautoscaling_policy" "freeswitch_session_count" { - name = "freeswitch-session-count-scale" - service_namespace = aws_appautoscaling_target.switch_scale_target.service_namespace - resource_id = aws_appautoscaling_target.switch_scale_target.resource_id - scalable_dimension = aws_appautoscaling_target.switch_scale_target.scalable_dimension - policy_type = "TargetTrackingScaling" - - target_tracking_scaling_policy_configuration { - customized_metric_specification { - metric_name = aws_cloudwatch_log_metric_filter.freeswitch_session_count.metric_transformation.*.name[0] - namespace = aws_cloudwatch_log_metric_filter.freeswitch_session_count.metric_transformation.*.namespace[0] - statistic = "Average" - unit = aws_cloudwatch_log_metric_filter.freeswitch_session_count.metric_transformation.*.unit[0] - } - - target_value = 100 - scale_in_cooldown = 300 - scale_out_cooldown = 60 - } -} - -resource "aws_cloudwatch_log_metric_filter" "freeswitch_session_count" { - name = "${var.switch_identifier}-SessionCount" - pattern = "{ $.Session-Count = * }" - log_group_name = aws_cloudwatch_log_group.freeswitch_event_logger.name - - metric_transformation { - name = "${var.switch_identifier}-SessionCount" - namespace = "SomlengSWITCH" - value = "$.Session-Count" - unit = "Count" - } -} - -# Route53 - -resource "aws_route53_record" "switch" { - zone_id = var.internal_route53_zone.zone_id - name = var.switch_subdomain - type = "A" - - alias { - name = var.internal_load_balancer.dns_name - zone_id = var.internal_load_balancer.zone_id - evaluate_target_health = true - } -} diff --git a/infrastructure/modules/somleng_switch/variables.tf b/infrastructure/modules/somleng_switch/variables.tf deleted file mode 100644 index 1d11601e9..000000000 --- a/infrastructure/modules/somleng_switch/variables.tf +++ /dev/null @@ -1,143 +0,0 @@ - -variable "aws_region" {} -variable "vpc" {} -variable "cluster_name" {} -variable "switch_identifier" {} -variable "services_identifier" {} -variable "s3_mpeg_identifier" {} -variable "public_gateway_identifier" {} -variable "client_gateway_identifier" {} -variable "media_proxy_identifier" {} -variable "app_environment" {} -variable "switch_app_image" {} -variable "nginx_image" {} -variable "freeswitch_image" {} -variable "opensips_scheduler_image" {} -variable "public_gateway_image" {} -variable "client_gateway_image" {} -variable "media_proxy_image" {} -variable "freeswitch_event_logger_image" {} -variable "s3_mpeg_ecr_repository_url" {} -variable "services_ecr_repository_url" {} -variable "internal_load_balancer" {} -variable "internal_listener" {} -variable "switch_subdomain" {} -variable "client_gateway_subdomain" {} -variable "route53_zone" {} -variable "internal_route53_zone" {} -variable "recordings_bucket_name" {} -variable "logs_bucket" {} -variable "efs_cache_name" {} -variable "global_accelerator" {} - -variable "container_insights_enabled" { - default = false -} -variable "assign_client_gateway_eips" { - default = true -} - -variable "assign_media_proxy_eips" { - default = false -} - -variable "switch_max_tasks" { - default = 4 -} -variable "switch_min_tasks" { - default = 1 -} -variable "public_gateway_max_tasks" { - default = 4 -} -variable "public_gateway_min_tasks" { - default = 1 -} -# This should be at least 2 to avoid tasks shutting down with -# clients still registered -variable "client_gateway_min_tasks" { - default = 2 -} -variable "client_gateway_max_tasks" { - default = 2 -} - -variable "media_proxy_min_tasks" { - default = 1 -} -variable "media_proxy_max_tasks" { - default = 4 -} - -variable "media_proxy_media_port_min" { - default = 30000 -} - -variable "media_proxy_media_port_max" { - default = 40000 -} - -variable "media_proxy_ng_port" { - default = 2223 -} -variable "media_proxy_healthcheck_port" { - default = 2224 -} - -# If the average CPU utilization over a minute drops to this threshold, -# the number of containers will be reduced (but not below ecs_autoscale_min_instances). -variable "ecs_as_cpu_low_threshold_per" { - default = "20" -} - -# If the average CPU utilization over a minute rises to this threshold, -# the number of containers will be increased (but not above ecs_autoscale_max_instances). -variable "ecs_as_cpu_high_threshold_per" { - default = "70" -} - -variable "freeswitch_event_socket_port" { - default = 8021 -} - -variable "sip_port" { - default = 5060 -} - -variable "sip_alternative_port" { - default = 5080 -} - -variable "switch_webserver_port" { - default = 80 -} - -variable "switch_appserver_port" { - default = 3000 -} - -variable "rayo_port" { - default = 5222 -} - -variable "redis_port" { - default = 6379 -} - -variable "opensips_fifo_name" { - default = "/var/opensips/opensips_fifo" -} - -variable "public_gateway_db_name" {} -variable "client_gateway_db_name" {} -variable "db_host" {} -variable "db_port" {} -variable "db_security_group" {} -variable "db_username" {} -variable "db_password_parameter_arn" {} - -variable "json_cdr_password_parameter_arn" {} -variable "external_rtp_ip" {} -variable "alternative_sip_outbound_ip" {} -variable "alternative_rtp_ip" {} -variable "json_cdr_url" {} diff --git a/infrastructure/modules/somleng_switch/versions.tf b/infrastructure/modules/somleng_switch/versions.tf deleted file mode 100644 index 397d09a2a..000000000 --- a/infrastructure/modules/somleng_switch/versions.tf +++ /dev/null @@ -1,17 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - local = { - source = "hashicorp/local" - } - tls = { - source = "hashicorp/tls" - } - docker = { - source = "kreuzwerker/docker" - } - } - required_version = ">= 0.13" -} diff --git a/infrastructure/modules/switch/autoscaling.tf b/infrastructure/modules/switch/autoscaling.tf new file mode 100644 index 000000000..65a4e55f9 --- /dev/null +++ b/infrastructure/modules/switch/autoscaling.tf @@ -0,0 +1,59 @@ +resource "aws_appautoscaling_target" "scale_target" { + service_namespace = "ecs" + resource_id = "service/${var.ecs_cluster.name}/${aws_ecs_service.this.name}" + scalable_dimension = "ecs:service:DesiredCount" + min_capacity = var.min_tasks + max_capacity = var.max_tasks +} + +resource "aws_appautoscaling_policy" "policy" { + name = "switch-scale" + service_namespace = aws_appautoscaling_target.scale_target.service_namespace + resource_id = aws_appautoscaling_target.scale_target.resource_id + scalable_dimension = aws_appautoscaling_target.scale_target.scalable_dimension + policy_type = "TargetTrackingScaling" + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "ECSServiceAverageCPUUtilization" + } + + target_value = 30 + scale_in_cooldown = 300 + scale_out_cooldown = 60 + } +} + +resource "aws_appautoscaling_policy" "freeswitch_session_count" { + name = "freeswitch-session-count-scale" + service_namespace = aws_appautoscaling_target.scale_target.service_namespace + resource_id = aws_appautoscaling_target.scale_target.resource_id + scalable_dimension = aws_appautoscaling_target.scale_target.scalable_dimension + policy_type = "TargetTrackingScaling" + + target_tracking_scaling_policy_configuration { + customized_metric_specification { + metric_name = aws_cloudwatch_log_metric_filter.freeswitch_session_count.metric_transformation.*.name[0] + namespace = aws_cloudwatch_log_metric_filter.freeswitch_session_count.metric_transformation.*.namespace[0] + statistic = "Average" + unit = aws_cloudwatch_log_metric_filter.freeswitch_session_count.metric_transformation.*.unit[0] + } + + target_value = 100 + scale_in_cooldown = 300 + scale_out_cooldown = 60 + } +} + +resource "aws_cloudwatch_log_metric_filter" "freeswitch_session_count" { + name = "${var.identifier}-SessionCount" + pattern = "{ $.Session-Count = * }" + log_group_name = aws_cloudwatch_log_group.freeswitch_event_logger.name + + metric_transformation { + name = "${var.identifier}-SessionCount" + namespace = "SomlengSWITCH" + value = "$.Session-Count" + unit = "Count" + } +} diff --git a/infrastructure/modules/switch/cache.tf b/infrastructure/modules/switch/cache.tf new file mode 100644 index 000000000..62244076f --- /dev/null +++ b/infrastructure/modules/switch/cache.tf @@ -0,0 +1,6 @@ +module "cache" { + source = "../efs" + vpc = var.region.vpc + name = var.cache_name + security_group_name = var.cache_security_group_name +} diff --git a/infrastructure/modules/switch/cloudwatch.tf b/infrastructure/modules/switch/cloudwatch.tf new file mode 100644 index 000000000..954800120 --- /dev/null +++ b/infrastructure/modules/switch/cloudwatch.tf @@ -0,0 +1,24 @@ +resource "aws_cloudwatch_log_group" "app" { + name = "${var.identifier}-app" + retention_in_days = 7 +} + +resource "aws_cloudwatch_log_group" "nginx" { + name = "${var.identifier}-nginx" + retention_in_days = 7 +} + +resource "aws_cloudwatch_log_group" "freeswitch" { + name = "${var.identifier}-freeswitch" + retention_in_days = 7 +} + +resource "aws_cloudwatch_log_group" "freeswitch_event_logger" { + name = "${var.identifier}-freeswitch-event-logger" + retention_in_days = 7 +} + +resource "aws_cloudwatch_log_group" "redis" { + name = "${var.identifier}-redis" + retention_in_days = 7 +} diff --git a/infrastructure/modules/switch/container_instances.tf b/infrastructure/modules/switch/container_instances.tf new file mode 100644 index 000000000..8f38cbbf7 --- /dev/null +++ b/infrastructure/modules/switch/container_instances.tf @@ -0,0 +1,10 @@ +module "container_instances" { + source = "../container_instances" + + app_identifier = var.identifier + vpc = var.region.vpc + instance_subnets = var.region.vpc.private_subnets + cluster_name = var.ecs_cluster.name + max_capacity = var.max_tasks * 2 + iam_instance_profile = var.container_instance_profile +} diff --git a/infrastructure/modules/switch/ecs.tf b/infrastructure/modules/switch/ecs.tf new file mode 100644 index 000000000..bc41b2ce4 --- /dev/null +++ b/infrastructure/modules/switch/ecs.tf @@ -0,0 +1,379 @@ +resource "aws_ecs_capacity_provider" "this" { + name = var.identifier + + auto_scaling_group_provider { + auto_scaling_group_arn = module.container_instances.autoscaling_group.arn + managed_termination_protection = "ENABLED" + managed_draining = "ENABLED" + + managed_scaling { + maximum_scaling_step_size = 1000 + minimum_scaling_step_size = 1 + status = "ENABLED" + target_capacity = 100 + } + } +} + +resource "aws_ecs_task_definition" "this" { + family = var.identifier + network_mode = "awsvpc" + requires_compatibilities = ["EC2"] + container_definitions = jsonencode([ + { + name = "nginx", + image = "${var.nginx_image}:latest", + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.nginx.name, + awslogs-region = var.region.aws_region, + awslogs-stream-prefix = var.app_environment + } + }, + essential = true, + portMappings = [ + { + containerPort = var.webserver_port, + protocol = "tcp" + } + ], + dependsOn = [ + { + containerName = "app", + condition = "HEALTHY" + } + ] + }, + { + name = "app", + image = "${var.app_image}:latest", + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.app.name, + awslogs-region = var.region.aws_region, + awslogs-stream-prefix = var.app_environment + } + }, + startTimeout = 120, + essential = true, + portMappings = [ + { + containerPort = var.appserver_port, + protocol = "tcp" + } + ], + dependsOn = [ + { + containerName = "redis", + condition = "HEALTHY" + } + ], + healthCheck = { + command = ["CMD-SHELL", "wget --server-response --spider --quiet http://localhost:$AHN_CORE_HTTP_PORT/health_checks 2>&1 | grep '200 OK' > /dev/null"], + interval = 10, + retries = 10, + timeout = 5 + }, + secrets = [ + { + name = "APP_MASTER_KEY", + valueFrom = local.application_master_key_parameter.arn + }, + { + name = "AHN_CORE_PASSWORD", + valueFrom = local.rayo_password_parameter.arn + } + ], + environment = [ + { + name = "AHN_ENV", + value = var.app_environment + }, + { + name = "APP_ENV", + value = var.app_environment + }, + { + name = "RACK_ENV", + value = var.app_environment + }, + { + name = "AWS_DEFAULT_REGION", + value = var.region.aws_region + }, + { + name = "AHN_CORE_HTTP_PORT", + value = tostring(var.appserver_port) + }, + { + name = "AHN_CORE_PORT", + value = tostring(var.rayo_port) + }, + { + name = "SERVICES_FUNCTION_ARN", + value = var.services_function.this.arn + }, + { + name = "REDIS_URL", + value = "redis://localhost:${var.redis_port}/1" + } + ] + }, + { + name = "freeswitch", + image = "${var.freeswitch_image}:latest", + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.freeswitch.name, + awslogs-region = var.region.aws_region, + awslogs-stream-prefix = var.app_environment + } + }, + startTimeout = 120, + healthCheck = { + command = [ + "CMD-SHELL", + "fs_cli -p $FS_EVENT_SOCKET_PASSWORD -x 'rayo status' | rayo_status_parser" + ] + interval = 10, + retries = 10 + timeout = 5 + } + essential = true, + portMappings = [ + { + containerPort = var.rayo_port, + protocol = "tcp" + }, + { + containerPort = var.sip_port, + protocol = "udp" + }, + { + containerPort = var.sip_alternative_port, + protocol = "udp" + }, + { + containerPort = var.freeswitch_event_socket_port, + protocol = "tcp" + } + ], + mountPoints = [ + { + containerPath = "/cache", + sourceVolume = "cache" + } + ], + secrets = [ + { + name = "FS_MOD_RAYO_PASSWORD", + valueFrom = local.rayo_password_parameter.arn + }, + { + name = "FS_MOD_JSON_CDR_PASSWORD", + valueFrom = var.json_cdr_password_parameter.arn + }, + { + name = "FS_RECORDINGS_BUCKET_ACCESS_KEY_ID", + valueFrom = local.recordings_bucket_access_key_id_parameter.arn + }, + { + name = "FS_RECORDINGS_BUCKET_SECRET_ACCESS_KEY", + valueFrom = local.recordings_bucket_secret_access_key_parameter.arn + }, + { + name = "FS_EVENT_SOCKET_PASSWORD", + valueFrom = local.freeswitch_event_socket_password_parameter.arn + } + ], + environment = [ + { + name = "AWS_DEFAULT_REGION", + value = var.region.aws_region + }, + { + name = "SERVICES_FUNCTION_REGION", + value = var.services_function.aws_region + }, + { + name = "FS_CACHE_DIRECTORY", + value = "/cache" + }, + { + name = "FS_STORAGE_DIRECTORY", + value = "/cache/freeswitch/storage" + }, + { + name = "FS_TTS_CACHE_DIRECTORY", + value = "/cache/freeswitch/tts_cache" + }, + { + name = "FS_LOG_DIRECTORY", + value = "/cache/freeswitch/logs" + }, + { + name = "FS_EXTERNAL_RTP_IP", + value = var.external_rtp_ip + }, + { + name = "FS_ALTERNATIVE_SIP_OUTBOUND_IP", + value = var.alternative_sip_outbound_ip + }, + { + name = "FS_ALTERNATIVE_RTP_IP", + value = var.alternative_rtp_ip + }, + { + name = "FS_MOD_RAYO_PORT", + value = tostring(var.rayo_port) + }, + { + name = "FS_MOD_JSON_CDR_URL", + value = var.json_cdr_url + }, + { + name = "FS_RECORDINGS_BUCKET_NAME", + value = local.recordings_bucket.id + }, + { + name = "FS_RECORDINGS_BUCKET_REGION", + value = local.recordings_bucket.region + }, + { + name = "FS_EVENT_SOCKET_PORT", + value = tostring(var.freeswitch_event_socket_port) + }, + { + name = "FS_SIP_PORT", + value = tostring(var.sip_port) + }, + { + name = "FS_SIP_ALTERNATIVE_PORT", + value = tostring(var.sip_alternative_port) + } + ] + }, + { + name = "redis", + image = "public.ecr.aws/docker/library/redis:alpine", + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.redis.name, + awslogs-region = var.region.aws_region, + awslogs-stream-prefix = var.app_environment + } + }, + essential = true, + healthCheck = { + command = ["CMD-SHELL", "redis-cli", "--raw", "incr", "ping"], + interval = 10, + retries = 10, + timeout = 5 + }, + portMappings = [ + { + containerPort = var.redis_port + } + ] + }, + { + name = "freeswitch-event-logger", + image = "${var.freeswitch_event_logger_image}:latest", + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.freeswitch_event_logger.name, + awslogs-region = var.region.aws_region, + awslogs-stream-prefix = var.app_environment + } + }, + startTimeout = 120, + essential = true, + secrets = [ + { + name = "EVENT_SOCKET_PASSWORD", + valueFrom = local.freeswitch_event_socket_password_parameter.arn + } + ], + dependsOn = [ + { + containerName = "freeswitch", + condition = "HEALTHY" + }, + { + containerName = "redis", + condition = "HEALTHY" + } + ], + environment = [ + { + name = "EVENT_SOCKET_HOST", + value = "localhost:${var.freeswitch_event_socket_port}" + }, + { + name = "REDIS_URL", + value = "redis://localhost:${var.redis_port}/1" + } + ] + } + ]) + + task_role_arn = local.iam_task_role.arn + execution_role_arn = local.iam_task_execution_role.arn + memory = module.container_instances.ec2_instance_type.memory_size - 512 + + volume { + name = "cache" + + efs_volume_configuration { + file_system_id = module.cache.file_system.id + transit_encryption = "ENABLED" + } + } +} + +resource "aws_ecs_service" "this" { + name = var.identifier + cluster = var.ecs_cluster.id + task_definition = aws_ecs_task_definition.this.arn + desired_count = var.min_tasks + + network_configuration { + subnets = var.region.vpc.private_subnets + security_groups = [ + aws_security_group.this.id + ] + } + + capacity_provider_strategy { + capacity_provider = aws_ecs_capacity_provider.this.name + weight = 1 + } + + placement_constraints { + type = "distinctInstance" + } + + load_balancer { + target_group_arn = aws_lb_target_group.this.arn + container_name = "nginx" + container_port = var.webserver_port + } + + deployment_circuit_breaker { + enable = true + rollback = true + } + + lifecycle { + ignore_changes = [task_definition, desired_count] + } + + depends_on = [ + aws_iam_role.ecs_task_role + ] +} diff --git a/infrastructure/modules/switch/event_bridge.tf b/infrastructure/modules/switch/event_bridge.tf new file mode 100644 index 000000000..d7c1069e2 --- /dev/null +++ b/infrastructure/modules/switch/event_bridge.tf @@ -0,0 +1,35 @@ +resource "aws_cloudwatch_event_rule" "ecs" { + name = "${var.identifier}-ecs-task-state-change" + + event_pattern = jsonencode({ + source = ["aws.ecs"], + detail-type = ["ECS Task State Change"], + detail = { + group = ["service:${var.identifier}"] + } + }) +} + +resource "aws_cloudwatch_event_target" "services" { + count = var.services_function.aws_region == var.region.aws_region ? 1 : 0 + + arn = var.services_function.this.arn + rule = aws_cloudwatch_event_rule.ecs.id +} + +resource "aws_lambda_permission" "this" { + count = var.services_function.aws_region == var.region.aws_region ? 1 : 0 + + action = "lambda:InvokeFunction" + function_name = var.services_function.this.arn + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.ecs.arn +} + +resource "aws_cloudwatch_event_target" "event_bus" { + count = var.services_function.aws_region != var.region.aws_region ? 1 : 0 + + arn = var.target_event_bus.this.arn + role_arn = var.target_event_bus.target_role.arn + rule = aws_cloudwatch_event_rule.ecs.id +} diff --git a/infrastructure/modules/switch/iam.tf b/infrastructure/modules/switch/iam.tf new file mode 100644 index 000000000..368ea19f7 --- /dev/null +++ b/infrastructure/modules/switch/iam.tf @@ -0,0 +1,96 @@ +locals { + create_iam_task_role = var.iam_task_role == null + create_iam_task_execution_role = var.iam_task_execution_role == null + iam_task_role = local.create_iam_task_role ? aws_iam_role.ecs_task_role[0] : var.iam_task_role + iam_task_execution_role = local.create_iam_task_execution_role ? aws_iam_role.task_execution_role[0] : var.iam_task_execution_role +} + +data "aws_iam_policy_document" "assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +# ECS Task Role + +resource "aws_iam_role" "ecs_task_role" { + count = local.create_iam_task_role ? 1 : 0 + name = "${var.identifier}-ecs-task-role" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "aws_iam_policy_document" "ecs_task_policy" { + statement { + effect = "Allow" + actions = ["polly:DescribeVoices", "polly:SynthesizeSpeech"] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = ["lambda:InvokeFunction"] + resources = [var.services_function.this.arn] + } +} + +resource "aws_iam_policy" "ecs_task_policy" { + count = local.create_iam_task_role ? 1 : 0 + name = "${var.identifier}-ecs-task-policy" + + policy = data.aws_iam_policy_document.ecs_task_policy.json +} + +resource "aws_iam_role_policy_attachment" "ecs_task_custom_policy" { + count = local.create_iam_task_role ? 1 : 0 + role = aws_iam_role.ecs_task_role[0].id + policy_arn = aws_iam_policy.ecs_task_policy[0].arn +} + +# ECS Task Execution Role + +resource "aws_iam_role" "task_execution_role" { + count = local.create_iam_task_execution_role ? 1 : 0 + name = "${var.identifier}-ecsTaskExecutionRole" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "aws_iam_policy_document" "task_execution_policy" { + statement { + effect = "Allow" + actions = ["ssm:GetParameters"] + resources = [ + local.application_master_key_parameter.arn, + local.rayo_password_parameter.arn, + local.freeswitch_event_socket_password_parameter.arn, + var.json_cdr_password_parameter.arn, + local.recordings_bucket_access_key_id_parameter.arn, + local.recordings_bucket_secret_access_key_parameter.arn + ] + } +} + +resource "aws_iam_policy" "task_execution_custom_policy" { + count = local.create_iam_task_execution_role ? 1 : 0 + name = "${var.identifier}-task-execution-custom-policy" + + policy = data.aws_iam_policy_document.task_execution_policy.json +} + +resource "aws_iam_role_policy_attachment" "task_execution_custom_policy" { + count = local.create_iam_task_execution_role ? 1 : 0 + role = aws_iam_role.task_execution_role[0].id + policy_arn = aws_iam_policy.task_execution_custom_policy[0].arn +} + +resource "aws_iam_role_policy_attachment" "task_execution_role_policy" { + count = local.create_iam_task_execution_role ? 1 : 0 + role = aws_iam_role.task_execution_role[0].id + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} diff --git a/infrastructure/modules/switch/lb.tf b/infrastructure/modules/switch/lb.tf new file mode 100644 index 000000000..bd6a35708 --- /dev/null +++ b/infrastructure/modules/switch/lb.tf @@ -0,0 +1,40 @@ +locals { + subdomain = "${var.identifier}.${var.region.alias}" + target_group_name = var.target_group_name == null ? "${var.identifier}-${var.region.alias}" : var.target_group_name +} + +resource "aws_lb_target_group" "this" { + name = local.target_group_name + port = var.webserver_port + protocol = "HTTP" + vpc_id = var.region.vpc.vpc_id + target_type = "ip" + deregistration_delay = 60 + + health_check { + protocol = "HTTP" + path = "/health_checks" + healthy_threshold = 3 + interval = 10 + } +} + +resource "aws_lb_listener_rule" "this" { + priority = var.lb_rule_index + listener_arn = var.region.internal_load_balancer.https_listener.arn + + action { + type = "forward" + target_group_arn = aws_lb_target_group.this.id + } + + condition { + host_header { + values = [aws_route53_record.this.fqdn] + } + } + + lifecycle { + ignore_changes = [action] + } +} diff --git a/infrastructure/modules/switch/outputs.tf b/infrastructure/modules/switch/outputs.tf new file mode 100644 index 000000000..ebb0abde2 --- /dev/null +++ b/infrastructure/modules/switch/outputs.tf @@ -0,0 +1,115 @@ +output "capacity_provider" { + value = aws_ecs_capacity_provider.this +} + +output "recordings_bucket" { + value = local.recordings_bucket +} + +output "recordings_bucket_access_key_id_parameter" { + value = local.recordings_bucket_access_key_id_parameter +} + +output "recordings_bucket_secret_access_key_parameter" { + value = local.recordings_bucket_secret_access_key_parameter +} + +output "application_master_key_parameter" { + value = local.application_master_key_parameter +} + +output "rayo_password_parameter" { + value = local.rayo_password_parameter +} + +output "freeswitch_event_socket_password_parameter" { + value = local.freeswitch_event_socket_password_parameter +} + +output "container_instances" { + value = module.container_instances +} + +output "iam_task_role" { + value = local.iam_task_role +} + +output "iam_task_execution_role" { + value = local.iam_task_execution_role +} + +output "identifier" { + value = var.identifier +} + +output "app_environment" { + value = var.app_environment +} + +output "json_cdr_url" { + value = var.json_cdr_url +} + +output "min_tasks" { + value = var.min_tasks +} + +output "max_tasks" { + value = var.max_tasks +} + +output "sip_port" { + value = var.sip_port +} + +output "cache_name" { + value = var.cache_name +} + +output "sip_alternative_port" { + value = var.sip_alternative_port +} + +output "freeswitch_event_socket_port" { + value = var.freeswitch_event_socket_port +} + +output "json_cdr_password_parameter" { + value = var.json_cdr_password_parameter +} + +output "services_function" { + value = var.services_function +} + +output "app_image" { + value = var.app_image +} + +output "nginx_image" { + value = var.nginx_image +} + +output "freeswitch_image" { + value = var.freeswitch_image +} + +output "freeswitch_event_logger_image" { + value = var.freeswitch_event_logger_image +} + +output "internal_route53_zone" { + value = var.internal_route53_zone +} + +output "target_group" { + value = aws_lb_target_group.this +} + +output "target_event_bus" { + value = var.target_event_bus == null ? var.region.event_bus : var.target_event_bus +} + +output "lb_rule_index" { + value = var.lb_rule_index +} diff --git a/infrastructure/modules/switch/providers.tf b/infrastructure/modules/switch/providers.tf new file mode 100644 index 000000000..f2702bf6e --- /dev/null +++ b/infrastructure/modules/switch/providers.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} diff --git a/infrastructure/modules/switch/route53.tf b/infrastructure/modules/switch/route53.tf new file mode 100644 index 000000000..c8191a2be --- /dev/null +++ b/infrastructure/modules/switch/route53.tf @@ -0,0 +1,11 @@ +resource "aws_route53_record" "this" { + zone_id = var.internal_route53_zone.zone_id + name = local.subdomain + type = "A" + + alias { + name = var.region.internal_load_balancer.this.dns_name + zone_id = var.region.internal_load_balancer.this.zone_id + evaluate_target_health = true + } +} diff --git a/infrastructure/modules/switch/s3.tf b/infrastructure/modules/switch/s3.tf new file mode 100644 index 000000000..ab6d04ce3 --- /dev/null +++ b/infrastructure/modules/switch/s3.tf @@ -0,0 +1,14 @@ +locals { + create_recordings_bucket = var.recordings_bucket == null && var.recordings_bucket_name != null + recordings_bucket = var.recordings_bucket != null ? var.recordings_bucket : module.recordings_bucket[0].this +} + +module "recordings_bucket" { + source = "../s3_bucket" + count = local.create_recordings_bucket ? 1 : 0 + + name = var.recordings_bucket_name + iam_username = "${var.identifier}_recordings" + access_key_id_parameter_name = var.recordings_bucket_access_key_id_parameter_name + secret_access_key_parameter_name = var.recordings_bucket_secret_access_key_parameter_name +} diff --git a/infrastructure/modules/switch/sg.tf b/infrastructure/modules/switch/sg.tf new file mode 100644 index 000000000..04ff817f4 --- /dev/null +++ b/infrastructure/modules/switch/sg.tf @@ -0,0 +1,62 @@ +resource "aws_security_group" "this" { + name = var.identifier + vpc_id = var.region.vpc.vpc_id + + tags = { + "Name" = var.identifier + } +} + +resource "aws_security_group_rule" "ingress_http" { + type = "ingress" + to_port = var.webserver_port + protocol = "TCP" + from_port = var.webserver_port + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "ingress_freeswitch_event_socket" { + type = "ingress" + to_port = 8021 + protocol = "TCP" + from_port = 8021 + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "ingress_sip" { + type = "ingress" + to_port = var.sip_port + protocol = "UDP" + from_port = var.sip_port + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "ingress_sip_alternative" { + type = "ingress" + to_port = var.sip_alternative_port + protocol = "UDP" + from_port = var.sip_alternative_port + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "icmp" { + type = "ingress" + to_port = -1 + protocol = "icmp" + from_port = -1 + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "egress" { + type = "egress" + to_port = 0 + protocol = "-1" + from_port = 0 + security_group_id = aws_security_group.this.id + cidr_blocks = ["0.0.0.0/0"] +} diff --git a/infrastructure/modules/switch/ssm.tf b/infrastructure/modules/switch/ssm.tf new file mode 100644 index 000000000..dd8164c69 --- /dev/null +++ b/infrastructure/modules/switch/ssm.tf @@ -0,0 +1,40 @@ +locals { + recordings_bucket_access_key_id_parameter = var.recordings_bucket_access_key_id_parameter != null ? var.recordings_bucket_access_key_id_parameter : module.recordings_bucket[0].access_key_id_parameter + recordings_bucket_secret_access_key_parameter = var.recordings_bucket_secret_access_key_parameter != null ? var.recordings_bucket_secret_access_key_parameter : module.recordings_bucket[0].secret_access_key_parameter + application_master_key_parameter = var.application_master_key_parameter != null ? var.application_master_key_parameter : aws_ssm_parameter.application_master_key[0] + rayo_password_parameter = var.rayo_password_parameter != null ? var.rayo_password_parameter : aws_ssm_parameter.rayo_password[0] + freeswitch_event_socket_password_parameter = var.freeswitch_event_socket_password_parameter != null ? var.freeswitch_event_socket_password_parameter : aws_ssm_parameter.freeswitch_event_socket_password[0] +} + +resource "aws_ssm_parameter" "application_master_key" { + count = var.application_master_key_parameter != null ? 0 : 1 + name = var.application_master_key_parameter_name + type = "SecureString" + value = "change-me" + + lifecycle { + ignore_changes = [value] + } +} + +resource "aws_ssm_parameter" "rayo_password" { + count = var.rayo_password_parameter != null ? 0 : 1 + name = var.rayo_password_parameter_name + type = "SecureString" + value = "change-me" + + lifecycle { + ignore_changes = [value] + } +} + +resource "aws_ssm_parameter" "freeswitch_event_socket_password" { + count = var.freeswitch_event_socket_password_parameter != null ? 0 : 1 + name = var.freeswitch_event_socket_password_parameter_name + type = "SecureString" + value = "change-me" + + lifecycle { + ignore_changes = [value] + } +} diff --git a/infrastructure/modules/switch/variables.tf b/infrastructure/modules/switch/variables.tf new file mode 100644 index 000000000..d8b636569 --- /dev/null +++ b/infrastructure/modules/switch/variables.tf @@ -0,0 +1,121 @@ +variable "identifier" {} +variable "ecs_cluster" {} +variable "app_environment" {} +variable "lb_rule_index" {} +variable "region" {} + +variable "target_group_name" { + default = null +} + +variable "recordings_bucket_name" { + default = null +} + +variable "recordings_bucket" { + default = null +} + +variable "recordings_bucket_access_key_id_parameter_name" { + default = null +} + +variable "recordings_bucket_access_key_id_parameter" { + default = null +} + +variable "recordings_bucket_secret_access_key_parameter_name" { + default = null +} + +variable "recordings_bucket_secret_access_key_parameter" { + default = null +} + +variable "application_master_key_parameter_name" { + default = null +} + +variable "application_master_key_parameter" { + default = null +} + +variable "rayo_password_parameter_name" { + default = null +} + +variable "rayo_password_parameter" { + default = null +} + +variable "freeswitch_event_socket_password_parameter_name" { + default = null +} + +variable "freeswitch_event_socket_password_parameter" { + default = null +} + +variable "container_instance_profile" { + default = null +} + +variable "iam_task_role" { + default = null +} + +variable "iam_task_execution_role" { + default = null +} + +variable "target_event_bus" { + default = null +} + +variable "json_cdr_password_parameter" {} +variable "services_function" {} +variable "cache_name" { + default = null +} +variable "cache_security_group_name" { + default = null +} +variable "internal_route53_zone" {} +variable "app_image" {} +variable "nginx_image" {} +variable "freeswitch_image" {} +variable "freeswitch_event_logger_image" {} +variable "external_rtp_ip" {} +variable "alternative_sip_outbound_ip" {} +variable "alternative_rtp_ip" {} +variable "json_cdr_url" {} +variable "route53_record" { + default = null +} +variable "sip_port" {} +variable "sip_alternative_port" {} + +variable "appserver_port" { + default = 3000 +} +variable "rayo_port" { + default = 5222 +} +variable "redis_port" { + default = 6379 +} +variable "freeswitch_event_socket_port" { + default = 8021 +} + +variable "max_tasks" { + default = 4 +} + +variable "min_tasks" { + default = 1 +} + +variable "webserver_port" { + default = 80 +} diff --git a/infrastructure/production/.terraform.lock.hcl b/infrastructure/production/.terraform.lock.hcl index 5256edf28..f41592b46 100644 --- a/infrastructure/production/.terraform.lock.hcl +++ b/infrastructure/production/.terraform.lock.hcl @@ -2,63 +2,24 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "5.55.0" + version = "5.63.1" hashes = [ - "h1:vChl08zNYLVzuSzfxz3wp3wNSx+vjwl/jPuyPbg59Ks=", - "zh:06fbb1cc4b61b9d6370d391bf7538aa6ef8b60b91c67d125a6be60a70b1d49f0", - "zh:1d52acd2184f379433a0fce2c29d5ed8fc7958d6a9d1b403310dcc36b2a3f626", - "zh:290bbce092f8836a1db530ac86d933cfea27d52b827639974a81bc48dfba8c34", - "zh:3531f2822c2de3ba837381c4ee4816c5b437fd204c07d659526a04d9154a65e8", - "zh:56d70db4c8c6c0ec1b665380b87726275f4ab3665b4b78ac86dc90e1010c0fe3", - "zh:8251d713c0b2c8c51b6858e51c70d083b484342ff9782a88c39e7eaa966c3da2", - "zh:9a7d1f7207e51382a7dd139dfd5786e7e905edf9bf89bbee4b59ad41365e87be", + "h1:Wu2MrBj79v8k4hMb8GKMu5p9KpVtCjNBsZ/R5wOTgZs=", + "zh:093adc21714d264005f66002464f4e9f48d6759adaaa88ca32db0c1134c2ca2b", + "zh:15505e01889d8da3e569ae3a8300cf12e8853822a5909a54eb07cf57f17daa74", + "zh:1c64ea9ab2c4a46a2e6eeafa4069106c1d9208aa2823264e58e826049b9417f7", + "zh:1ca7e98446f519f08ad684928b8bc22d480e419b6210955af8a31730d8dbc5ad", + "zh:3bd8fe53647e17fadcfe13536160009e4bb77e1c2fe224e991c82fb228ab4ece", + "zh:68d4bd6ffba3c6484c228a1756b1c7c16802ebd58a20b8d6bfb547d96a2eaa69", + "zh:68fbabfc04bde3655ded9919f5954ab8884a35d265d41aec53f95804e741ca7c", + "zh:69c2ea737c1cfb7252f22ca7a50d8cc7a4729ea288fc3833933c2380023ca605", + "zh:796caec3b4e8d177e5e4787d7b61a8a541993edc33db2c3ffffdfdbbad3967b5", + "zh:877a02805e1b4503b4e174a34084055873619af9d9e57e7098c27d0e0be0b592", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a529c78dfc60063289524690af78794e99a768835b88e27cdfec15bc85439f7c", - "zh:b6da1843355db05c5d412126406fd97db2a6ff9edc166b81c1cea2994535b4eb", - "zh:bfc08cd23b1556b3287d1b28ac7f12c7d459471d97a0592bf2579ea68d11bae7", - "zh:c382088faf05894191636b57861069a21de10a5ff4eb8f7cc122e764ccf7a4a8", - "zh:e27f99f389921314ee428b24990d3a829057ce532b2beb33c69387458722edd9", - "zh:ef11285eedb45ffc3fb2ecdfefa206e64eb2760a87fff15c44dee42de9703436", - "zh:fedc4ebee0d6fe196691127004db5d1ff8bd22e3b667a74026bb92c607589b6c", - ] -} - -provider "registry.terraform.io/hashicorp/local" { - version = "2.5.1" - hashes = [ - "h1:/GAVA/xheGQcbOZEq0qxANOg+KVLCA7Wv8qluxhTjhU=", - "zh:0af29ce2b7b5712319bf6424cb58d13b852bf9a777011a545fac99c7fdcdf561", - "zh:126063ea0d79dad1f68fa4e4d556793c0108ce278034f101d1dbbb2463924561", - "zh:196bfb49086f22fd4db46033e01655b0e5e036a5582d250412cc690fa7995de5", - "zh:37c92ec084d059d37d6cffdb683ccf68e3a5f8d2eb69dd73c8e43ad003ef8d24", - "zh:4269f01a98513651ad66763c16b268f4c2da76cc892ccfd54b401fff6cc11667", - "zh:51904350b9c728f963eef0c28f1d43e73d010333133eb7f30999a8fb6a0cc3d8", - "zh:73a66611359b83d0c3fcba2984610273f7954002febb8a57242bbb86d967b635", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7ae387993a92bcc379063229b3cce8af7eaf082dd9306598fcd42352994d2de0", - "zh:9e0f365f807b088646db6e4a8d4b188129d9ebdbcf2568c8ab33bddd1b82c867", - "zh:b5263acbd8ae51c9cbffa79743fbcadcb7908057c87eb22fd9048268056efbc4", - "zh:dfcd88ac5f13c0d04e24be00b686d069b4879cc4add1b7b1a8ae545783d97520", - ] -} - -provider "registry.terraform.io/hashicorp/tls" { - version = "4.0.5" - hashes = [ - "h1:yLqz+skP3+EbU3yyvw8JqzflQTKDQGsC9QyZAg+S4dg=", - "h1:zeG5RmggBZW/8JWIVrdaeSJa0OG62uFX5HY1eE8SjzY=", - "zh:01cfb11cb74654c003f6d4e32bbef8f5969ee2856394a96d127da4949c65153e", - "zh:0472ea1574026aa1e8ca82bb6df2c40cd0478e9336b7a8a64e652119a2fa4f32", - "zh:1a8ddba2b1550c5d02003ea5d6cdda2eef6870ece86c5619f33edd699c9dc14b", - "zh:1e3bb505c000adb12cdf60af5b08f0ed68bc3955b0d4d4a126db5ca4d429eb4a", - "zh:6636401b2463c25e03e68a6b786acf91a311c78444b1dc4f97c539f9f78de22a", - "zh:76858f9d8b460e7b2a338c477671d07286b0d287fd2d2e3214030ae8f61dd56e", - "zh:a13b69fb43cb8746793b3069c4d897bb18f454290b496f19d03c3387d1c9a2dc", - "zh:a90ca81bb9bb509063b736842250ecff0f886a91baae8de65c8430168001dad9", - "zh:c4de401395936e41234f1956ebadbd2ed9f414e6908f27d578614aaa529870d4", - "zh:c657e121af8fde19964482997f0de2d5173217274f6997e16389e7707ed8ece8", - "zh:d68b07a67fbd604c38ec9733069fbf23441436fecf554de6c75c032f82e1ef19", - "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:d37f14e0807d73eff3a8384d694b4e770d41ae3286b5195927d9d809076a2d68", + "zh:e45279ca14b28647ac26dc8ca87f67da994f961e92ad316c9bc71be922c0a3fb", + "zh:e63eb4cc5b78319a26120bdce985f44ac4b1e71e43abac0eca4eaceb0af570f5", + "zh:f5c12695fcd777825434aa7aa560b6e1d851f823d75fda7c9df5c177071720a5", ] } diff --git a/infrastructure/production/client_gateway.tf b/infrastructure/production/client_gateway.tf new file mode 100644 index 000000000..35c1e83f1 --- /dev/null +++ b/infrastructure/production/client_gateway.tf @@ -0,0 +1,26 @@ +module "client_gateway" { + source = "../modules/client_gateway" + + subdomain = "sip" + + identifier = var.client_gateway_identifier + app_environment = var.app_environment + + aws_region = var.aws_default_region + vpc = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc + ecs_cluster = aws_ecs_cluster.this + route53_zone = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_somleng_org + + app_image = data.terraform_remote_state.core.outputs.client_gateway_ecr_repository.repository_uri + scheduler_image = data.terraform_remote_state.core.outputs.opensips_scheduler_ecr_repository.repository_uri + + db_security_group = data.terraform_remote_state.core_infrastructure.outputs.db_security_group + sip_port = var.sip_port + + db_password_parameter = data.terraform_remote_state.core_infrastructure.outputs.db_master_password_parameter + db_name = var.client_gateway_db_name + db_username = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.master_username + db_host = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.endpoint + db_port = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.port + services_function = module.services +} diff --git a/infrastructure/production/ecs.tf b/infrastructure/production/ecs.tf new file mode 100644 index 000000000..c8fff2699 --- /dev/null +++ b/infrastructure/production/ecs.tf @@ -0,0 +1,30 @@ +resource "aws_ecs_cluster" "this" { + name = var.ecs_cluster_name +} + +resource "aws_ecs_cluster_capacity_providers" "this" { + cluster_name = aws_ecs_cluster.this.name + + capacity_providers = [ + module.switch.capacity_provider.name, + module.public_gateway.capacity_provider.name, + module.client_gateway.capacity_provider.name, + module.media_proxy.capacity_provider.name + ] +} + +resource "aws_ecs_cluster" "helium" { + name = var.ecs_cluster_name + + provider = aws.helium +} + +resource "aws_ecs_cluster_capacity_providers" "helium" { + cluster_name = aws_ecs_cluster.helium.name + + capacity_providers = [ + module.switch_helium.capacity_provider.name + ] + + provider = aws.helium +} diff --git a/infrastructure/production/main.tf b/infrastructure/production/main.tf deleted file mode 100644 index 261c95017..000000000 --- a/infrastructure/production/main.tf +++ /dev/null @@ -1,62 +0,0 @@ -data "aws_ssm_parameter" "somleng_services_password" { - name = "somleng.production.services_password" -} - -module "somleng_switch" { - source = "../modules/somleng_switch" - - cluster_name = "somleng-switch" - switch_identifier = "switch" - services_identifier = "switch-services" - s3_mpeg_identifier = "s3-mpeg" - public_gateway_identifier = "public-gateway" - client_gateway_identifier = "client-gateway" - media_proxy_identifier = "media-proxy" - - switch_app_image = data.terraform_remote_state.core.outputs.switch_ecr_repository.repository_uri - nginx_image = data.terraform_remote_state.core.outputs.nginx_ecr_repository.repository_uri - freeswitch_image = data.terraform_remote_state.core.outputs.freeswitch_ecr_repository.repository_uri - freeswitch_event_logger_image = data.terraform_remote_state.core.outputs.freeswitch_event_logger_ecr_repository.repository_uri - public_gateway_image = data.terraform_remote_state.core.outputs.public_gateway_ecr_repository.repository_uri - client_gateway_image = data.terraform_remote_state.core.outputs.client_gateway_ecr_repository.repository_uri - media_proxy_image = data.terraform_remote_state.core.outputs.media_proxy_ecr_repository.repository_uri - opensips_scheduler_image = data.terraform_remote_state.core.outputs.opensips_scheduler_ecr_repository.repository_uri - s3_mpeg_ecr_repository_url = data.terraform_remote_state.core.outputs.s3_mpeg_ecr_repository.repository_url - services_ecr_repository_url = data.terraform_remote_state.core.outputs.services_ecr_repository.repository_url - - vpc = data.terraform_remote_state.core_infrastructure.outputs.vpc - - aws_region = var.aws_region - app_environment = "production" - - json_cdr_password_parameter_arn = data.aws_ssm_parameter.somleng_services_password.arn - json_cdr_url = "https://api.internal.somleng.org/services/call_data_records" - external_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.vpc.nat_public_ips[0] - - alternative_sip_outbound_ip = data.terraform_remote_state.core_infrastructure.outputs.nat_instance_ip - alternative_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.nat_instance_ip - - efs_cache_name = "somleng-switch-cache" - public_gateway_db_name = "opensips_public_gateway" - client_gateway_db_name = "opensips_client_gateway" - db_username = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.master_username - db_password_parameter_arn = data.terraform_remote_state.core_infrastructure.outputs.db_master_password_parameter.arn - db_host = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.endpoint - db_port = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.port - db_security_group = data.terraform_remote_state.core_infrastructure.outputs.db_security_group.id - - internal_load_balancer = data.terraform_remote_state.core_infrastructure.outputs.internal_application_load_balancer - internal_listener = data.terraform_remote_state.core_infrastructure.outputs.internal_https_listener - global_accelerator = data.terraform_remote_state.core_infrastructure.outputs.global_accelerator - - logs_bucket = data.terraform_remote_state.core_infrastructure.outputs.logs_bucket - - route53_zone = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_somleng_org - internal_route53_zone = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_internal_somleng_org - - switch_subdomain = "switch" - client_gateway_subdomain = "sip" - - recordings_bucket_name = "raw-recordings.somleng.org" - switch_max_tasks = 10 -} diff --git a/infrastructure/production/media_proxy.tf b/infrastructure/production/media_proxy.tf new file mode 100644 index 000000000..1741b62de --- /dev/null +++ b/infrastructure/production/media_proxy.tf @@ -0,0 +1,11 @@ +module "media_proxy" { + source = "../modules/media_proxy" + + identifier = var.media_proxy_identifier + app_environment = var.app_environment + aws_region = var.aws_default_region + + vpc = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc + ecs_cluster = aws_ecs_cluster.this + app_image = data.terraform_remote_state.core.outputs.media_proxy_ecr_repository.repository_uri +} diff --git a/infrastructure/production/public_gateway.tf b/infrastructure/production/public_gateway.tf new file mode 100644 index 000000000..8b5d6d6dc --- /dev/null +++ b/infrastructure/production/public_gateway.tf @@ -0,0 +1,25 @@ +module "public_gateway" { + source = "../modules/public_gateway" + + identifier = var.public_gateway_identifier + app_environment = var.app_environment + + aws_region = var.aws_default_region + vpc = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc + ecs_cluster = aws_ecs_cluster.this + + app_image = data.terraform_remote_state.core.outputs.public_gateway_ecr_repository.repository_uri + scheduler_image = data.terraform_remote_state.core.outputs.opensips_scheduler_ecr_repository.repository_uri + + sip_port = var.sip_port + sip_alternative_port = var.sip_alternative_port + + db_security_group = data.terraform_remote_state.core_infrastructure.outputs.db_security_group + db_password_parameter = data.terraform_remote_state.core_infrastructure.outputs.db_master_password_parameter + db_name = var.public_gateway_db_name + db_username = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.master_username + db_host = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.endpoint + db_port = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.port + global_accelerator = data.terraform_remote_state.core_infrastructure.outputs.global_accelerator + logs_bucket = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.logs_bucket +} diff --git a/infrastructure/production/s3_mpeg.tf b/infrastructure/production/s3_mpeg.tf new file mode 100644 index 000000000..c8e04ece9 --- /dev/null +++ b/infrastructure/production/s3_mpeg.tf @@ -0,0 +1,7 @@ +module "s3_mpeg" { + source = "../modules/s3_mpeg" + + identifier = var.s3_mpeg_identifier + app_image = data.terraform_remote_state.core.outputs.s3_mpeg_ecr_repository.repository_url + recordings_bucket = module.switch.recordings_bucket +} diff --git a/infrastructure/production/services.tf b/infrastructure/production/services.tf new file mode 100644 index 000000000..8c5f71b4f --- /dev/null +++ b/infrastructure/production/services.tf @@ -0,0 +1,26 @@ +module "services" { + source = "../modules/services" + + identifier = var.services_identifier + app_environment = var.app_environment + switch_group = var.switch_identifier + media_proxy_group = var.media_proxy_identifier + client_gateway_group = var.client_gateway_identifier + public_gateway_db_name = var.public_gateway_db_name + client_gateway_db_name = var.client_gateway_db_name + + vpc = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc + app_image = data.terraform_remote_state.core.outputs.services_ecr_repository.repository_url + + db_password_parameter = data.terraform_remote_state.core_infrastructure.outputs.db_master_password_parameter + freeswitch_event_socket_password_parameter = data.aws_ssm_parameter.freeswitch_event_socket_password + + db_security_group = data.terraform_remote_state.core_infrastructure.outputs.db_security_group + db_username = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.master_username + db_host = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.endpoint + db_port = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.port + sip_port = var.sip_port + sip_alternative_port = var.sip_alternative_port + freeswitch_event_socket_port = var.freeswitch_event_socket_port + media_proxy_ng_port = module.media_proxy.ng_port +} diff --git a/infrastructure/production/ssm.tf b/infrastructure/production/ssm.tf new file mode 100644 index 000000000..feb6758f4 --- /dev/null +++ b/infrastructure/production/ssm.tf @@ -0,0 +1,7 @@ +data "aws_ssm_parameter" "somleng_services_password" { + name = "somleng.production.services_password" +} + +data "aws_ssm_parameter" "freeswitch_event_socket_password" { + name = "somleng-switch.production.freeswitch_event_socket_password" +} diff --git a/infrastructure/production/switch.tf b/infrastructure/production/switch.tf new file mode 100644 index 000000000..770614b25 --- /dev/null +++ b/infrastructure/production/switch.tf @@ -0,0 +1,106 @@ +module "switch" { + source = "../modules/switch" + + json_cdr_url = "https://api.somleng.org/services/call_data_records" + target_group_name = "switch-internal" + cache_name = "somleng-switch-cache" + cache_security_group_name = "switch-efs-cache" + recordings_bucket_name = "raw-recordings.somleng.org" + application_master_key_parameter_name = "somleng-switch.${var.app_environment}.application_master_key" + rayo_password_parameter_name = "somleng-switch.${var.app_environment}.rayo_password" + freeswitch_event_socket_password_parameter_name = "somleng-switch.${var.app_environment}.freeswitch_event_socket_password" + recordings_bucket_access_key_id_parameter_name = "somleng-switch.${var.app_environment}.recordings_bucket_access_key_id" + recordings_bucket_secret_access_key_parameter_name = "somleng-switch.${var.app_environment}.recordings_bucket_secret_access_key" + max_tasks = 10 + identifier = var.switch_identifier + app_environment = var.app_environment + region = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region + ecs_cluster = aws_ecs_cluster.this + sip_port = var.sip_port + sip_alternative_port = var.sip_alternative_port + freeswitch_event_socket_port = var.freeswitch_event_socket_port + json_cdr_password_parameter = data.aws_ssm_parameter.somleng_services_password + services_function = module.services + internal_route53_zone = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_internal_somleng_org + lb_rule_index = 20 + app_image = data.terraform_remote_state.core.outputs.switch_ecr_repository.repository_uri + nginx_image = data.terraform_remote_state.core.outputs.nginx_ecr_repository.repository_uri + freeswitch_image = data.terraform_remote_state.core.outputs.freeswitch_ecr_repository.repository_uri + freeswitch_event_logger_image = data.terraform_remote_state.core.outputs.freeswitch_event_logger_ecr_repository.repository_uri + external_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc.nat_public_ips[0] + alternative_sip_outbound_ip = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.nat_instance.public_ip + alternative_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.nat_instance.public_ip +} + +module "switch_helium" { + source = "../modules/switch" + + region = data.terraform_remote_state.core_infrastructure.outputs.helium_region + ecs_cluster = aws_ecs_cluster.helium + external_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.helium_region.vpc.nat_public_ips[0] + alternative_sip_outbound_ip = data.terraform_remote_state.core_infrastructure.outputs.helium_region.vpc.nat_public_ips[0] + alternative_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.helium_region.vpc.nat_public_ips[0] + identifier = module.switch.identifier + lb_rule_index = module.switch.lb_rule_index + app_environment = module.switch.app_environment + json_cdr_url = module.switch.json_cdr_url + cache_name = module.switch.cache_name + recordings_bucket = module.switch.recordings_bucket + recordings_bucket_access_key_id_parameter = module.switch.recordings_bucket_access_key_id_parameter + recordings_bucket_secret_access_key_parameter = module.switch.recordings_bucket_secret_access_key_parameter + application_master_key_parameter = module.switch.application_master_key_parameter + rayo_password_parameter = module.switch.rayo_password_parameter + freeswitch_event_socket_password_parameter = module.switch.freeswitch_event_socket_password_parameter + container_instance_profile = module.switch.container_instances.iam_instance_profile + iam_task_role = module.switch.iam_task_role + iam_task_execution_role = module.switch.iam_task_execution_role + min_tasks = module.switch.min_tasks + max_tasks = module.switch.max_tasks + sip_port = module.switch.sip_port + sip_alternative_port = module.switch.sip_alternative_port + freeswitch_event_socket_port = module.switch.freeswitch_event_socket_port + json_cdr_password_parameter = module.switch.json_cdr_password_parameter + services_function = module.switch.services_function + app_image = module.switch.app_image + nginx_image = module.switch.nginx_image + freeswitch_image = module.switch.freeswitch_image + freeswitch_event_logger_image = module.switch.freeswitch_event_logger_image + internal_route53_zone = module.switch.internal_route53_zone + target_event_bus = module.switch.target_event_bus + + providers = { + aws = aws.helium + } +} + +resource "aws_route53_record" "switch_legacy" { + zone_id = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_internal_somleng_org_old.zone_id + name = "switch" + type = "A" + + alias { + name = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.internal_load_balancer.this.dns_name + zone_id = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.internal_load_balancer.this.zone_id + evaluate_target_health = true + } +} + +resource "aws_lb_listener_rule" "switch_legacy" { + priority = 30 + listener_arn = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.internal_load_balancer.https_listener.arn + + action { + type = "forward" + target_group_arn = module.switch.target_group.id + } + + condition { + host_header { + values = [aws_route53_record.switch_legacy.fqdn] + } + } + + lifecycle { + ignore_changes = [action] + } +} diff --git a/infrastructure/production/terraform.tf b/infrastructure/production/terraform.tf index c86381190..9878a0f06 100644 --- a/infrastructure/production/terraform.tf +++ b/infrastructure/production/terraform.tf @@ -8,7 +8,12 @@ terraform { } provider "aws" { - region = var.aws_region + region = var.aws_default_region +} + +provider "aws" { + region = var.aws_helium_region + alias = "helium" } data "terraform_remote_state" "core" { @@ -17,7 +22,7 @@ data "terraform_remote_state" "core" { config = { bucket = "infrastructure.somleng.org" key = "somleng_switch_core.tfstate" - region = var.aws_region + region = var.aws_default_region } } @@ -27,6 +32,6 @@ data "terraform_remote_state" "core_infrastructure" { config = { bucket = "infrastructure.somleng.org" key = "core.tfstate" - region = var.aws_region + region = var.aws_default_region } } diff --git a/infrastructure/production/variables.tf b/infrastructure/production/variables.tf index aaa8897e9..3263a759e 100644 --- a/infrastructure/production/variables.tf +++ b/infrastructure/production/variables.tf @@ -1,3 +1,59 @@ -variable "aws_region" { +variable "aws_default_region" { default = "ap-southeast-1" } + +variable "aws_helium_region" { + default = "us-east-1" +} + +variable "app_environment" { + default = "production" +} + +variable "ecs_cluster_name" { + default = "somleng-switch" +} + +variable "switch_identifier" { + default = "switch" +} + +variable "services_identifier" { + default = "switch-services" +} + +variable "s3_mpeg_identifier" { + default = "s3-mpeg" +} + +variable "public_gateway_identifier" { + default = "public-gateway" +} + +variable "client_gateway_identifier" { + default = "client-gateway" +} + +variable "media_proxy_identifier" { + default = "media-proxy" +} + +variable "client_gateway_db_name" { + default = "opensips_client_gateway" +} + +variable "public_gateway_db_name" { + default = "opensips_public_gateway" +} + +variable "sip_port" { + default = 5060 +} + +variable "sip_alternative_port" { + default = 5080 +} + +variable "freeswitch_event_socket_port" { + default = 8021 +} diff --git a/infrastructure/staging/.terraform.lock.hcl b/infrastructure/staging/.terraform.lock.hcl index 5256edf28..57314f78c 100644 --- a/infrastructure/staging/.terraform.lock.hcl +++ b/infrastructure/staging/.terraform.lock.hcl @@ -23,45 +23,6 @@ provider "registry.terraform.io/hashicorp/aws" { ] } -provider "registry.terraform.io/hashicorp/local" { - version = "2.5.1" - hashes = [ - "h1:/GAVA/xheGQcbOZEq0qxANOg+KVLCA7Wv8qluxhTjhU=", - "zh:0af29ce2b7b5712319bf6424cb58d13b852bf9a777011a545fac99c7fdcdf561", - "zh:126063ea0d79dad1f68fa4e4d556793c0108ce278034f101d1dbbb2463924561", - "zh:196bfb49086f22fd4db46033e01655b0e5e036a5582d250412cc690fa7995de5", - "zh:37c92ec084d059d37d6cffdb683ccf68e3a5f8d2eb69dd73c8e43ad003ef8d24", - "zh:4269f01a98513651ad66763c16b268f4c2da76cc892ccfd54b401fff6cc11667", - "zh:51904350b9c728f963eef0c28f1d43e73d010333133eb7f30999a8fb6a0cc3d8", - "zh:73a66611359b83d0c3fcba2984610273f7954002febb8a57242bbb86d967b635", - "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7ae387993a92bcc379063229b3cce8af7eaf082dd9306598fcd42352994d2de0", - "zh:9e0f365f807b088646db6e4a8d4b188129d9ebdbcf2568c8ab33bddd1b82c867", - "zh:b5263acbd8ae51c9cbffa79743fbcadcb7908057c87eb22fd9048268056efbc4", - "zh:dfcd88ac5f13c0d04e24be00b686d069b4879cc4add1b7b1a8ae545783d97520", - ] -} - -provider "registry.terraform.io/hashicorp/tls" { - version = "4.0.5" - hashes = [ - "h1:yLqz+skP3+EbU3yyvw8JqzflQTKDQGsC9QyZAg+S4dg=", - "h1:zeG5RmggBZW/8JWIVrdaeSJa0OG62uFX5HY1eE8SjzY=", - "zh:01cfb11cb74654c003f6d4e32bbef8f5969ee2856394a96d127da4949c65153e", - "zh:0472ea1574026aa1e8ca82bb6df2c40cd0478e9336b7a8a64e652119a2fa4f32", - "zh:1a8ddba2b1550c5d02003ea5d6cdda2eef6870ece86c5619f33edd699c9dc14b", - "zh:1e3bb505c000adb12cdf60af5b08f0ed68bc3955b0d4d4a126db5ca4d429eb4a", - "zh:6636401b2463c25e03e68a6b786acf91a311c78444b1dc4f97c539f9f78de22a", - "zh:76858f9d8b460e7b2a338c477671d07286b0d287fd2d2e3214030ae8f61dd56e", - "zh:a13b69fb43cb8746793b3069c4d897bb18f454290b496f19d03c3387d1c9a2dc", - "zh:a90ca81bb9bb509063b736842250ecff0f886a91baae8de65c8430168001dad9", - "zh:c4de401395936e41234f1956ebadbd2ed9f414e6908f27d578614aaa529870d4", - "zh:c657e121af8fde19964482997f0de2d5173217274f6997e16389e7707ed8ece8", - "zh:d68b07a67fbd604c38ec9733069fbf23441436fecf554de6c75c032f82e1ef19", - "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - ] -} - provider "registry.terraform.io/kreuzwerker/docker" { version = "3.0.2" hashes = [ diff --git a/infrastructure/staging/client_gateway.tf b/infrastructure/staging/client_gateway.tf new file mode 100644 index 000000000..5e67550a4 --- /dev/null +++ b/infrastructure/staging/client_gateway.tf @@ -0,0 +1,29 @@ +module "client_gateway" { + source = "../modules/client_gateway" + + subdomain = "sip-staging" + + identifier = var.client_gateway_identifier + app_environment = var.app_environment + + aws_region = var.aws_default_region + vpc = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc + ecs_cluster = aws_ecs_cluster.this + route53_zone = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_somleng_org + + app_image = data.terraform_remote_state.core.outputs.client_gateway_ecr_repository.repository_uri + scheduler_image = data.terraform_remote_state.core.outputs.opensips_scheduler_ecr_repository.repository_uri + min_tasks = 0 + max_tasks = 2 + + db_security_group = data.terraform_remote_state.core_infrastructure.outputs.db_security_group + assign_eips = false + sip_port = var.sip_port + + db_password_parameter = data.terraform_remote_state.core_infrastructure.outputs.db_master_password_parameter + db_name = var.client_gateway_db_name + db_username = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.master_username + db_host = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.endpoint + db_port = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.port + services_function = module.services +} diff --git a/infrastructure/staging/ecs.tf b/infrastructure/staging/ecs.tf new file mode 100644 index 000000000..c8fff2699 --- /dev/null +++ b/infrastructure/staging/ecs.tf @@ -0,0 +1,30 @@ +resource "aws_ecs_cluster" "this" { + name = var.ecs_cluster_name +} + +resource "aws_ecs_cluster_capacity_providers" "this" { + cluster_name = aws_ecs_cluster.this.name + + capacity_providers = [ + module.switch.capacity_provider.name, + module.public_gateway.capacity_provider.name, + module.client_gateway.capacity_provider.name, + module.media_proxy.capacity_provider.name + ] +} + +resource "aws_ecs_cluster" "helium" { + name = var.ecs_cluster_name + + provider = aws.helium +} + +resource "aws_ecs_cluster_capacity_providers" "helium" { + cluster_name = aws_ecs_cluster.helium.name + + capacity_providers = [ + module.switch_helium.capacity_provider.name + ] + + provider = aws.helium +} diff --git a/infrastructure/staging/main.tf b/infrastructure/staging/main.tf deleted file mode 100644 index 34075e347..000000000 --- a/infrastructure/staging/main.tf +++ /dev/null @@ -1,75 +0,0 @@ -data "aws_ssm_parameter" "somleng_services_password" { - name = "somleng.staging.services_password" -} - -module "somleng_switch_staging" { - source = "../modules/somleng_switch" - - cluster_name = "somleng-switch-staging" - switch_identifier = "switch-staging" - services_identifier = "switch-services-staging" - s3_mpeg_identifier = "s3-mpeg-staging" - public_gateway_identifier = "public-gateway-staging" - client_gateway_identifier = "client-gateway-staging" - media_proxy_identifier = "media-proxy-staging" - - aws_region = var.aws_region - app_environment = "staging" - - switch_app_image = data.terraform_remote_state.core.outputs.switch_ecr_repository.repository_uri - nginx_image = data.terraform_remote_state.core.outputs.nginx_ecr_repository.repository_uri - freeswitch_image = data.terraform_remote_state.core.outputs.freeswitch_ecr_repository.repository_uri - freeswitch_event_logger_image = data.terraform_remote_state.core.outputs.freeswitch_event_logger_ecr_repository.repository_uri - public_gateway_image = data.terraform_remote_state.core.outputs.public_gateway_ecr_repository.repository_uri - client_gateway_image = data.terraform_remote_state.core.outputs.client_gateway_ecr_repository.repository_uri - media_proxy_image = data.terraform_remote_state.core.outputs.media_proxy_ecr_repository.repository_uri - opensips_scheduler_image = data.terraform_remote_state.core.outputs.opensips_scheduler_ecr_repository.repository_uri - - s3_mpeg_ecr_repository_url = data.terraform_remote_state.core.outputs.s3_mpeg_ecr_repository.repository_url - services_ecr_repository_url = data.terraform_remote_state.core.outputs.services_ecr_repository.repository_url - - vpc = data.terraform_remote_state.core_infrastructure.outputs.vpc - - json_cdr_password_parameter_arn = data.aws_ssm_parameter.somleng_services_password.arn - json_cdr_url = "https://api-staging.internal.somleng.org/services/call_data_records" - external_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.vpc.nat_public_ips[0] - - alternative_sip_outbound_ip = data.terraform_remote_state.core_infrastructure.outputs.nat_instance_ip - alternative_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.nat_instance_ip - - efs_cache_name = "switch-staging-cache" - public_gateway_db_name = "opensips_public_gateway_staging" - client_gateway_db_name = "opensips_client_gateway_staging" - db_username = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.master_username - db_password_parameter_arn = data.terraform_remote_state.core_infrastructure.outputs.db_master_password_parameter.arn - db_host = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.endpoint - db_port = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.port - db_security_group = data.terraform_remote_state.core_infrastructure.outputs.db_security_group.id - - internal_load_balancer = data.terraform_remote_state.core_infrastructure.outputs.internal_application_load_balancer - internal_listener = data.terraform_remote_state.core_infrastructure.outputs.internal_https_listener - - route53_zone = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_somleng_org - internal_route53_zone = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_internal_somleng_org - global_accelerator = data.terraform_remote_state.core_infrastructure.outputs.global_accelerator - - logs_bucket = data.terraform_remote_state.core_infrastructure.outputs.logs_bucket - - switch_subdomain = "switch-staging" - client_gateway_subdomain = "sip-staging" - - recordings_bucket_name = "raw-recordings-staging.somleng.org" - - sip_port = 6060 - sip_alternative_port = 6080 - switch_min_tasks = 0 - switch_max_tasks = 2 - public_gateway_min_tasks = 0 - public_gateway_max_tasks = 2 - client_gateway_min_tasks = 0 - client_gateway_max_tasks = 2 - media_proxy_min_tasks = 0 - media_proxy_max_tasks = 2 - assign_client_gateway_eips = false - assign_media_proxy_eips = false -} diff --git a/infrastructure/staging/media_proxy.tf b/infrastructure/staging/media_proxy.tf new file mode 100644 index 000000000..78aae89e6 --- /dev/null +++ b/infrastructure/staging/media_proxy.tf @@ -0,0 +1,14 @@ +module "media_proxy" { + source = "../modules/media_proxy" + + identifier = var.media_proxy_identifier + app_environment = var.app_environment + + aws_region = var.aws_default_region + vpc = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc + ecs_cluster = aws_ecs_cluster.this + app_image = data.terraform_remote_state.core.outputs.media_proxy_ecr_repository.repository_uri + + min_tasks = 0 + max_tasks = 2 +} diff --git a/infrastructure/staging/public_gateway.tf b/infrastructure/staging/public_gateway.tf new file mode 100644 index 000000000..6b304f5b3 --- /dev/null +++ b/infrastructure/staging/public_gateway.tf @@ -0,0 +1,28 @@ +module "public_gateway" { + source = "../modules/public_gateway" + + identifier = var.public_gateway_identifier + app_environment = var.app_environment + + aws_region = var.aws_default_region + vpc = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc + + ecs_cluster = aws_ecs_cluster.this + + app_image = data.terraform_remote_state.core.outputs.public_gateway_ecr_repository.repository_uri + scheduler_image = data.terraform_remote_state.core.outputs.opensips_scheduler_ecr_repository.repository_uri + min_tasks = 0 + max_tasks = 2 + + sip_port = var.sip_port + sip_alternative_port = var.sip_alternative_port + + db_security_group = data.terraform_remote_state.core_infrastructure.outputs.db_security_group + db_password_parameter = data.terraform_remote_state.core_infrastructure.outputs.db_master_password_parameter + db_name = var.public_gateway_db_name + db_username = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.master_username + db_host = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.endpoint + db_port = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.port + global_accelerator = data.terraform_remote_state.core_infrastructure.outputs.global_accelerator + logs_bucket = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.logs_bucket +} diff --git a/infrastructure/staging/s3_mpeg.tf b/infrastructure/staging/s3_mpeg.tf new file mode 100644 index 000000000..c8e04ece9 --- /dev/null +++ b/infrastructure/staging/s3_mpeg.tf @@ -0,0 +1,7 @@ +module "s3_mpeg" { + source = "../modules/s3_mpeg" + + identifier = var.s3_mpeg_identifier + app_image = data.terraform_remote_state.core.outputs.s3_mpeg_ecr_repository.repository_url + recordings_bucket = module.switch.recordings_bucket +} diff --git a/infrastructure/staging/services.tf b/infrastructure/staging/services.tf new file mode 100644 index 000000000..8c5f71b4f --- /dev/null +++ b/infrastructure/staging/services.tf @@ -0,0 +1,26 @@ +module "services" { + source = "../modules/services" + + identifier = var.services_identifier + app_environment = var.app_environment + switch_group = var.switch_identifier + media_proxy_group = var.media_proxy_identifier + client_gateway_group = var.client_gateway_identifier + public_gateway_db_name = var.public_gateway_db_name + client_gateway_db_name = var.client_gateway_db_name + + vpc = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc + app_image = data.terraform_remote_state.core.outputs.services_ecr_repository.repository_url + + db_password_parameter = data.terraform_remote_state.core_infrastructure.outputs.db_master_password_parameter + freeswitch_event_socket_password_parameter = data.aws_ssm_parameter.freeswitch_event_socket_password + + db_security_group = data.terraform_remote_state.core_infrastructure.outputs.db_security_group + db_username = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.master_username + db_host = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.endpoint + db_port = data.terraform_remote_state.core_infrastructure.outputs.db_cluster.port + sip_port = var.sip_port + sip_alternative_port = var.sip_alternative_port + freeswitch_event_socket_port = var.freeswitch_event_socket_port + media_proxy_ng_port = module.media_proxy.ng_port +} diff --git a/infrastructure/staging/ssm.tf b/infrastructure/staging/ssm.tf new file mode 100644 index 000000000..718982140 --- /dev/null +++ b/infrastructure/staging/ssm.tf @@ -0,0 +1,7 @@ +data "aws_ssm_parameter" "somleng_services_password" { + name = "somleng.staging.services_password" +} + +data "aws_ssm_parameter" "freeswitch_event_socket_password" { + name = "somleng-switch.staging.freeswitch_event_socket_password" +} diff --git a/infrastructure/staging/switch.tf b/infrastructure/staging/switch.tf new file mode 100644 index 000000000..236ead808 --- /dev/null +++ b/infrastructure/staging/switch.tf @@ -0,0 +1,107 @@ +module "switch" { + source = "../modules/switch" + + json_cdr_url = "https://api-staging.somleng.org/services/call_data_records" + target_group_name = "switch-staging-internal" + cache_name = "switch-staging-cache" + cache_security_group_name = "switch-staging-efs-cache" + recordings_bucket_name = "raw-recordings-staging.somleng.org" + application_master_key_parameter_name = "somleng-switch.${var.app_environment}.application_master_key" + rayo_password_parameter_name = "somleng-switch.${var.app_environment}.rayo_password" + freeswitch_event_socket_password_parameter_name = "somleng-switch.${var.app_environment}.freeswitch_event_socket_password" + recordings_bucket_access_key_id_parameter_name = "somleng-switch.${var.app_environment}.recordings_bucket_access_key_id" + recordings_bucket_secret_access_key_parameter_name = "somleng-switch.${var.app_environment}.recordings_bucket_secret_access_key" + min_tasks = 0 + max_tasks = 2 + lb_rule_index = 120 + identifier = var.switch_identifier + app_environment = var.app_environment + region = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region + ecs_cluster = aws_ecs_cluster.this + sip_port = var.sip_port + sip_alternative_port = var.sip_alternative_port + freeswitch_event_socket_port = var.freeswitch_event_socket_port + json_cdr_password_parameter = data.aws_ssm_parameter.somleng_services_password + services_function = module.services + internal_route53_zone = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_internal_somleng_org + app_image = data.terraform_remote_state.core.outputs.switch_ecr_repository.repository_uri + nginx_image = data.terraform_remote_state.core.outputs.nginx_ecr_repository.repository_uri + freeswitch_image = data.terraform_remote_state.core.outputs.freeswitch_ecr_repository.repository_uri + freeswitch_event_logger_image = data.terraform_remote_state.core.outputs.freeswitch_event_logger_ecr_repository.repository_uri + external_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.vpc.nat_public_ips[0] + alternative_sip_outbound_ip = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.nat_instance.public_ip + alternative_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.nat_instance.public_ip +} + +module "switch_helium" { + source = "../modules/switch" + + region = data.terraform_remote_state.core_infrastructure.outputs.helium_region + ecs_cluster = aws_ecs_cluster.helium + external_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.helium_region.vpc.nat_public_ips[0] + alternative_sip_outbound_ip = data.terraform_remote_state.core_infrastructure.outputs.helium_region.vpc.nat_public_ips[0] + alternative_rtp_ip = data.terraform_remote_state.core_infrastructure.outputs.helium_region.vpc.nat_public_ips[0] + identifier = module.switch.identifier + lb_rule_index = module.switch.lb_rule_index + app_environment = module.switch.app_environment + json_cdr_url = module.switch.json_cdr_url + cache_name = module.switch.cache_name + recordings_bucket = module.switch.recordings_bucket + recordings_bucket_access_key_id_parameter = module.switch.recordings_bucket_access_key_id_parameter + recordings_bucket_secret_access_key_parameter = module.switch.recordings_bucket_secret_access_key_parameter + application_master_key_parameter = module.switch.application_master_key_parameter + rayo_password_parameter = module.switch.rayo_password_parameter + freeswitch_event_socket_password_parameter = module.switch.freeswitch_event_socket_password_parameter + container_instance_profile = module.switch.container_instances.iam_instance_profile + iam_task_role = module.switch.iam_task_role + iam_task_execution_role = module.switch.iam_task_execution_role + min_tasks = module.switch.min_tasks + max_tasks = module.switch.max_tasks + sip_port = module.switch.sip_port + sip_alternative_port = module.switch.sip_alternative_port + freeswitch_event_socket_port = module.switch.freeswitch_event_socket_port + json_cdr_password_parameter = module.switch.json_cdr_password_parameter + services_function = module.switch.services_function + app_image = module.switch.app_image + nginx_image = module.switch.nginx_image + freeswitch_image = module.switch.freeswitch_image + freeswitch_event_logger_image = module.switch.freeswitch_event_logger_image + internal_route53_zone = module.switch.internal_route53_zone + target_event_bus = module.switch.target_event_bus + + providers = { + aws = aws.helium + } +} + +resource "aws_route53_record" "switch_legacy" { + zone_id = data.terraform_remote_state.core_infrastructure.outputs.route53_zone_internal_somleng_org_old.zone_id + name = "switch-staging" + type = "A" + + alias { + name = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.internal_load_balancer.this.dns_name + zone_id = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.internal_load_balancer.this.zone_id + evaluate_target_health = true + } +} + +resource "aws_lb_listener_rule" "switch_legacy" { + priority = 130 + listener_arn = data.terraform_remote_state.core_infrastructure.outputs.hydrogen_region.internal_load_balancer.https_listener.arn + + action { + type = "forward" + target_group_arn = module.switch.target_group.id + } + + condition { + host_header { + values = [aws_route53_record.switch_legacy.fqdn] + } + } + + lifecycle { + ignore_changes = [action] + } +} diff --git a/infrastructure/staging/terraform.tf b/infrastructure/staging/terraform.tf index c49cda35d..b00b2308b 100644 --- a/infrastructure/staging/terraform.tf +++ b/infrastructure/staging/terraform.tf @@ -8,7 +8,12 @@ terraform { } provider "aws" { - region = var.aws_region + region = var.aws_default_region +} + +provider "aws" { + region = var.aws_helium_region + alias = "helium" } data "terraform_remote_state" "core" { @@ -17,7 +22,7 @@ data "terraform_remote_state" "core" { config = { bucket = "infrastructure.somleng.org" key = "somleng_switch_core.tfstate" - region = var.aws_region + region = var.aws_default_region } } @@ -27,6 +32,6 @@ data "terraform_remote_state" "core_infrastructure" { config = { bucket = "infrastructure.somleng.org" key = "core.tfstate" - region = var.aws_region + region = var.aws_default_region } } diff --git a/infrastructure/staging/variables.tf b/infrastructure/staging/variables.tf index aaa8897e9..aec8fb103 100644 --- a/infrastructure/staging/variables.tf +++ b/infrastructure/staging/variables.tf @@ -1,3 +1,59 @@ -variable "aws_region" { +variable "aws_default_region" { default = "ap-southeast-1" } + +variable "aws_helium_region" { + default = "us-east-1" +} + +variable "ecs_cluster_name" { + default = "somleng-switch-staging" +} + +variable "app_environment" { + default = "staging" +} + +variable "switch_identifier" { + default = "switch-staging" +} + +variable "services_identifier" { + default = "switch-services-staging" +} + +variable "s3_mpeg_identifier" { + default = "s3-mpeg-staging" +} + +variable "public_gateway_identifier" { + default = "public-gateway-staging" +} + +variable "client_gateway_identifier" { + default = "client-gateway-staging" +} + +variable "media_proxy_identifier" { + default = "media-proxy-staging" +} + +variable "client_gateway_db_name" { + default = "opensips_client_gateway_staging" +} + +variable "public_gateway_db_name" { + default = "opensips_public_gateway_staging" +} + +variable "sip_port" { + default = 6060 +} + +variable "sip_alternative_port" { + default = 6080 +} + +variable "freeswitch_event_socket_port" { + default = 8021 +}