Skip to content

Commit

Permalink
feat: add terraform and ansible files as well as generation commands
Browse files Browse the repository at this point in the history
  • Loading branch information
MikaAK committed Aug 29, 2022
1 parent 35c46ee commit ac620fe
Show file tree
Hide file tree
Showing 34 changed files with 826 additions and 53 deletions.
2 changes: 2 additions & 0 deletions lib/deploy_ex.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
defmodule DeployEx do
end
2 changes: 0 additions & 2 deletions lib/learn_elixir_deploys.ex

This file was deleted.

45 changes: 45 additions & 0 deletions lib/mix/deploy_ex_helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule DeployExHelpers do
def app_name, do: Mix.Project.get() |> Module.split |> hd
def underscored_app_name, do: Macro.underscore(app_name)

def check_in_umbrella do
if Mix.Project.umbrella?() do
:ok
else
{:error, ErrorMessage.bad_request("must be in umbrella root")}
end
end

def priv_file(priv_subdirectory) do
:deploy_ex
|> :code.priv_dir
|> Path.join(priv_subdirectory)
end

def write_file(file_path, contents, opts) do
if opts[:message] do
if opts[:force] || Mix.Generator.overwrite?(file_path, contents) do
File.write!(file_path, contents)

if !opts[:quiet] do
Mix.shell().info(opts[:message])
end
end
else
Mix.Generator.create_file(file_path, contents, opts)
end
end

def check_file_exists!(file_path) do
if !File.exists?(file_path) do
raise to_string(IO.ANSI.format([
:red, "Cannot find ",
:bright, "#{file_path}", :reset
]))
end
end

def upper_title_case(string) do
string |> String.split("_") |> Enum.map_join(" ", &String.capitalize/1)
end
end
10 changes: 1 addition & 9 deletions lib/mix/tasks/ansible.build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,14 @@ defmodule Mix.Tasks.Ansible.Build do
"""

def run(args) do
with :ok <- check_in_umbrella(),
with :ok <- DeployExHelpers.check_in_umbrella(),
:ok <- create_ansible_hosts_file(parse_args(args)) do
:ok
else
{:error, e} -> Mix.shell().error(to_string(e))
end
end

defp check_in_umbrella do
if Mix.Project.umbrella?() do
:ok
else
{:error, ErrorMessage.bad_request("must be in umbrella root")}
end
end

defp parse_args(args) do
{opts, _} = OptionParser.parse!(args,
aliases: [f: :force, q: :quit],
Expand Down
168 changes: 136 additions & 32 deletions lib/mix/tasks/terraform.build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,171 @@ defmodule Mix.Tasks.Terraform.Build do
Deploys to ansible
"""

@terraform_file Path.expand("../../deploys/terraform/variables.tf")

if not File.exists?(@terraform_file) do
raise "Terraform file doesn't exist at #{@terraform_file}"
end
@terraform_default_path "./deploys/terraform"
@default_aws_region "us-west-2"

def run(args) do
terraform_output = LearnElixir.MixProject.releases()
|> Keyword.keys
|> Enum.map_join(",\n\n", &(&1 |> to_string |> generate_terraform_output))
opts = args
|> parse_args
|> Keyword.put_new(:directory, @terraform_default_path)
|> Keyword.put_new(:aws_region, @default_aws_region)
|> Keyword.put_new(:env, Mix.env())

opts = opts
|> Keyword.put_new(:variables_file, Path.join(opts[:directory], "variables.tf"))
|> Keyword.put_new(:main_file, Path.join(opts[:directory], "main.tf"))

opts = parse_args(args)
with :ok <- DeployExHelpers.check_in_umbrella(),
{:ok, releases} <- fetch_mix_releases(),
:ok <- ensure_terraform_directory_exists(opts[:directory]) do
terraform_output = releases
|> Keyword.keys
|> Enum.map_join(",\n\n", &(&1 |> to_string |> generate_terraform_output))

write_to_terraform(terraform_output, opts)
write_terraform_variables(terraform_output, opts)
write_terraform_main(opts)
end
end

defp fetch_mix_releases do
case Mix.Project.get() do
nil -> {:error, ErrorMessage.not_found("couldn't find mix project")}
project -> {:ok, project.releases()}
end
end

defp parse_args(args) do
{opts, _} = OptionParser.parse!(args,
aliases: [f: :force, q: :quit],
{opts, _extra_args} = OptionParser.parse!(args,
aliases: [f: :force, q: :quit, d: :directory, v: :verbose],
switches: [
directory: :string,
force: :boolean,
quiet: :boolean
quiet: :boolean,
verbose: :boolean
]
)

opts
end

defp ensure_terraform_directory_exists(directory) do
if File.exists?(directory) do
:ok
else
Mix.shell().info([:green, "Copying terraform into #{directory}"])

"terraform"
|> DeployExHelpers.priv_file()
|> File.cp_r!(directory)

:ok
end
end

defp generate_terraform_output(release_name) do
"""
String.trim_trailing("""
#{release_name} = {
environment = "prod"
name = "#{title_case(release_name)}"
environment = "#{Mix.env()}"
name = "#{DeployExHelpers.upper_title_case(release_name)}"
}
"""
end

defp title_case(string) do
string |> String.split("_") |> Enum.map_join(" ", &String.capitalize/1)
""", "\n")
end

defp write_to_terraform(terraform_output, opts) do
new_terraform_file = @terraform_file |> File.read! |> inject_terraform_contents(terraform_output)
defp write_terraform_variables(terraform_output, opts) do
if File.exists?(opts[:variables_file]) do
opts[:variables_file]
|> File.read!
|> inject_terraform_contents_into_variables(terraform_output, opts)
else
:deploy_ex
|> :code.priv_dir
|> Path.join("terraform/variables.tf.eex")
|> File.cp!("#{opts[:variables_file]}.eex")

if opts[:force] || Mix.Generator.overwrite?(@terraform_file, new_terraform_file) do
File.write!(@terraform_file, new_terraform_file)

if !opts[:quiet] do
Mix.shell().info([:green, "* injecting ", :reset, @terraform_file])
end
generate_and_delete_variables_template(terraform_output, opts)
end
end

defp inject_terraform_contents(current_file, terraform_output) do
defp inject_terraform_contents_into_variables(current_file, terraform_output, opts) do
current_file = String.split(current_file, "\n")
project_variable_idx = Enum.find_index(
current_file,
&(&1 =~ "variable \"learn_elixir_project\"")
&(&1 =~ "variable \"#{DeployExHelpers.underscored_app_name()}_project\"")
) + 4 # 4 is the number of newlines till the default key
{start_of_file, project_variable} = Enum.split(current_file, project_variable_idx + 1)

Enum.join(start_of_file ++ String.split(terraform_output, "\n") ++ Enum.take(project_variable, -3), "\n")
new_file = Enum.join(start_of_file ++ String.split(terraform_output, "\n") ++ Enum.take(project_variable, -3), "\n")

if new_file !== current_file do
opts = [{:message, [:green, "* injecting ", :reset, opts[:variables_file]]} | opts]

DeployExHelpers.write_file(opts[:variables_file], new_file, opts)
end
end

defp generate_and_delete_variables_template(terraform_output, opts) do
template_file = "#{opts[:variables_file]}.eex"

DeployExHelpers.check_file_exists!(template_file)

variables_files = EEx.eval_file(template_file, assigns: %{
environment: opts[:env],
terraform_release_variables: terraform_output,
app_name: DeployExHelpers.underscored_app_name()
})

DeployExHelpers.write_file(opts[:variables_file], variables_files, opts)

if opts[:verbose] do
Mix.shell().info([:green, "* removing ", :reset, template_file])
end

File.rm!(template_file)
end

defp write_terraform_main(opts) do
if File.exists?(opts[:main_file]) do
rewrite_terraform_main_contents(opts)
else
generate_and_delete_main_template(opts)
end
end

defp rewrite_terraform_main_contents(opts) do
main_file_path = "terraform"
|> Path.join(Path.basename(opts[:main_file]))
|> DeployExHelpers.priv_file
|> Path.expand
|> Kernel.<>(".eex")

DeployExHelpers.check_file_exists!(main_file_path)

File.cp!(main_file_path, "#{opts[:main_file]}.eex")

opts
|> Keyword.put(:message, [
:green, "* copying template to ", :reset, "#{opts[:main_file]}.eex\n",
:green, "* rewriting ", :reset, opts[:main_file]
])
|> generate_and_delete_main_template
end

defp generate_and_delete_main_template(opts) do
template_file_path = "#{opts[:main_file]}.eex"

DeployExHelpers.check_file_exists!(template_file_path)

main_file = EEx.eval_file(template_file_path, assigns: %{
aws_region: opts[:aws_region],
app_name: DeployExHelpers.underscored_app_name()
})

DeployExHelpers.write_file(opts[:main_file], main_file, opts)

if opts[:verbose] do
Mix.shell().info([:green, "* removing ", :reset, template_file_path])
end

File.rm!(template_file_path)
end
end
8 changes: 2 additions & 6 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
defmodule LearnElixirDeploys.MixProject do
defmodule DeployEx.MixProject do
use Mix.Project

def project do
[
app: :learn_elixir_deploys,
app: :deploy_ex,
version: "0.1.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.13",
start_permanent: Mix.env() == :prod,
deps: deps()
Expand Down
8 changes: 8 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
%{
"error_message": {:hex, :error_message, "0.2.1", "81ecf0b5384d0643d6256526409b06faca62aedfd256a755b52b36e16f2ab725", [:mix], [{:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "36c500b5dcfb4a2fb671d912fe8bb9e90e3f058a18d4712c0698e8d81de23340"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
}
7 changes: 7 additions & 0 deletions priv/ansible/ansible.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[defaults]
remote_user = admin
inventory = ./hosts
deprecation_warnings = False

[inventory]
enable_plugins = yaml
15 changes: 15 additions & 0 deletions priv/ansible/hosts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
all:
children:

learn_elixir_assignment_marker:
hosts:
52.89.6.55:

learn_elixir_lander:
hosts:
52.12.176.114:

learn_elixir_tasks:
hosts:
54.244.125.38:

7 changes: 7 additions & 0 deletions priv/ansible/hosts.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
all:<% import Mix.Tasks.Ansible.Build %>
children:
<%= for {host_name, host_ips} <- @host_name_ips do %>
<%= host_name %>:
hosts:
<%= for host_ip <- host_ips do %><%= host_ip %>:
<% end %><% end %>
41 changes: 41 additions & 0 deletions priv/ansible/roles/awscli/tasks/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
- name: awscli
block:
- name: Check if awscli is installed
stat:
path: /usr/local/bin/aws
register: awscli_exe

- name: Check if awscli is updated to {{ awscli_version }}
shell: aws --version | cut -d ' ' -f 1 | cut -d '/' -f 2
register: awscli_current_version
when: awscli_exe.stat.exists
changed_when: false

- name: Install unzip
apt:
name: unzip
# update_cache: true

- name: Download awscli installer
unarchive:
src: https://awscli.amazonaws.com/awscli-exe-linux-x86_64-{{ awscli_version }}.zip
dest: /tmp
remote_src: true
when: (not awscli_exe.stat.exists) or (awscli_current_version.stdout != awscli_version)

- name: Install awscli {{ awscli_version }}
command: ./aws/install --update
args:
chdir: /tmp
when: (not awscli_exe.stat.exists) or (awscli_current_version.stdout != awscli_version)

- name: Install boto3
pip:
name: boto3
executable: pip3

- name: Cleanup awscli installer
file:
path: /tmp/aws
state: absent
become: true
2 changes: 2 additions & 0 deletions priv/ansible/roles/beam_linux_tuning/files/limits.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* soft nofile 20000000
* hard nofile 20000000
2 changes: 2 additions & 0 deletions priv/ansible/roles/beam_linux_tuning/files/rc.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
defrt=`ip route | grep "^default" | head -1`
ip route change $defrt initcwnd 10
Loading

0 comments on commit ac620fe

Please sign in to comment.