Packages

Clustering strategy for connecting nodes running on Google App Engine.

Current section

Files

Jump to
libcluster_gae lib google_app_engine.ex
Raw

lib/google_app_engine.ex

defmodule Cluster.Strategy.GoogleAppEngine do
@moduledoc """
Clustering strategy for Google App Engine.
This strategy checks for the list of app versions that are currently receiving HTTP.
For each version that is listed, the list of instances running for that version are fetched.
Once all of the instances have been received, they attempt to connect to each other.
**Note**: This strategy only connects nodes that are able to receive HTTP traffic.
Here's an example configuration:
```elixir
config :libcluster,
topologies: [
my_app: [
strategy: Cluster.Strategy.GoogleAppEngine,
config: [
polling_interval: 10_000
]
]
]
```
## Configurable Options
Options can be set for the strategy under the `:config` key when defining the topology.
* `:polling_interval` - Interval for checking for the list of running instances. Defaults to `10_000`
## Application Setup
### Google Cloud
Enable the **App Engine Admin API** for your application's Google Cloud Project. Follow the guide on [enabling APIs](https://cloud.google.com/apis/docs/enable-disable-apis).
### Release Configuration
Update your release's `vm.args` file to include the following lines.
```
## Name of the node
-sname <%= release_name %>@${GAE_INSTANCE}
## Limit distributed erlang ports to a single port
-kernel inet_dist_listen_min 9999
-kernel inet_dist_listen_max 9999
```
### GAE Configuration File
Update the `app.yaml` configuration file for Google App Engine.
```yaml
env_variables:
REPLACE_OS_VARS: true
network:
forwarded_ports:
# epmd
- 4369
# erlang distribution
- 9999
```
"""
use GenServer
use Cluster.Strategy
alias Cluster.Strategy.State
@default_polling_interval 10_000
@access_token_path 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'
def start_link(args) do
GenServer.start_link(__MODULE__, args)
end
@impl true
def init([%State{} = state]) do
{:ok, load(state)}
end
@impl true
def handle_info(:timeout, state) do
handle_info(:load, state)
end
def handle_info(:load, %State{} = state) do
{:noreply, load(state)}
end
def handle_info(_, state) do
{:noreply, state}
end
defp load(%State{} = state) do
connect = state.connect
list_nodes = state.list_nodes
topology = state.topology
nodes = get_nodes(state)
Cluster.Strategy.connect_nodes(topology, connect, list_nodes, nodes)
Process.send_after(self(), :load, polling_interval(state))
state
end
defp polling_interval(%State{config: config}) do
Keyword.get(config, :polling_interval, @default_polling_interval)
end
defp get_nodes(%State{}) do
instances = get_running_instances()
release_name = System.get_env("REL_NAME")
Enum.map(instances, & :"#{release_name}@#{&1}")
end
defp get_running_instances do
project_id = System.get_env("GOOGLE_CLOUD_PROJECT")
service_id = System.get_env("GAE_SERVICE")
versions = get_running_versions(project_id, service_id)
Enum.flat_map(versions, &get_instances_for_version(project_id, service_id, &1))
end
defp get_running_versions(project_id, service_id) do
access_token = access_token()
headers = [{'Authorization', 'Bearer #{access_token}'}]
api_url = 'https://appengine.googleapis.com/v1/apps/#{project_id}/services/#{service_id}'
case :httpc.request(:get, {api_url, headers}, [], []) do
{:ok, {{_, 200, _}, _headers, body}} ->
%{"split" => %{"allocations" => allocations}} = Jason.decode!(body)
Map.keys(allocations)
end
end
defp get_instances_for_version(project_id, service_id, version) do
access_token = access_token()
headers = [{'Authorization', 'Bearer #{access_token}'}]
api_url = 'https://appengine.googleapis.com/v1/apps/#{project_id}/services/#{service_id}/versions/#{version}/instances'
case :httpc.request(:get, {api_url, headers}, [], []) do
{:ok, {{_, 200, _}, _headers, body}} ->
handle_instances(Jason.decode!(body))
end
end
defp handle_instances(%{"instances" => instances}) do
instances
|> Enum.filter(& &1["vmStatus"] == "RUNNING")
|> Enum.map(& &1["id"])
end
defp handle_instances(_), do: []
defp access_token do
headers = [{'Metadata-Flavor', 'Google'}]
case :httpc.request(:get, {@access_token_path, headers}, [], []) do
{:ok, {{_, 200, _}, _headers, body}} ->
%{"access_token" => token} = Jason.decode!(body)
token
end
end
end