Skip to content

Commit

Permalink
Add "ContinuousIntegrationDeployment" chapter
Browse files Browse the repository at this point in the history
  • Loading branch information
Gabriella439 committed Aug 12, 2023
1 parent c01b3f6 commit 5989c6b
Show file tree
Hide file tree
Showing 11 changed files with 906 additions and 10 deletions.
1 change: 1 addition & 0 deletions manuscript/BigPicture.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Some of these potential improvements are not specific to the Nix ecosystem. Aft

You can save yourself a lot of headaches by taking time to learn and use the Nix ecosystem as idiomatically as possible instead of learning these lessons the hard way.

{id: gitops}
## GitOps

NixOS exemplifies the [Infrastructure as Code (IaC)](https://en.wikipedia.org/wiki/Infrastructure_as_code) paradigm, meaning that every aspect of your organization (including hardware/systems/software) is stored in code or configuration files that are the source of truth for how everything is built. In particular, you don't make undocumented changes to your infrastructure that cause it to diverge from what is recorded within those files.
Expand Down
1 change: 1 addition & 0 deletions manuscript/Book.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ WebServer.md
Modules.md
AdvancedModules.md
Terraform.md
ContinuousIntegrationDeployment.md
640 changes: 640 additions & 0 deletions manuscript/ContinuousIntegrationDeployment.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions manuscript/Modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ true

This illustrates that our NixOS module really is just a function whose input is an attribute set and whose output is also an attribute set. There is nothing special about this function other than it happens to be the same shape as what the NixOS module system accepts.

{id: nixos}
## NixOS

So if NixOS modules are just pure functions or pure attribute sets, what turns those functions or attribute sets into a useful operating system? In other words, what puts the "NixOS" in the "NixOS module system"?
Expand Down
1 change: 1 addition & 0 deletions manuscript/Setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ For example, the above command uses support for process substitution (which is n

Now that you've installed Nix I'll show you how to launch a NixOS virtual machine (VM) so that you can easily test the examples throughout this book.

{id: darwin-builder}
### macOS-specific instructions

If you are using macOS, then follow the instructions in the [Nixpkgs manual](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder) to set up a local Linux builder. We'll need this builder to create other NixOS machines, since they require Linux build products.
Expand Down
37 changes: 29 additions & 8 deletions manuscript/Terraform.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{id: terraform}
# Deploying to AWS using Terraform

Up until now we've been playing things safe and test-driving everything locally on our own machine. We could even prolong this for quite a while because NixOS has advanced support for building and testing clusters of NixOS machines locally using virtual machines. However, at some point we need to dive in and deploy a server if we're going to use NixOS for real.
Expand Down Expand Up @@ -266,7 +267,10 @@ resource "tls_private_key" "nixos-in-production" {
}
resource "local_sensitive_file" "ssh_key_file" {
resource "local_sensitive_file" "ssh_private_key" {
}
resource "local_file" "ssh_public_key" {
}
resource "aws_key_pair" "nixos-in-production" {
Expand Down Expand Up @@ -301,7 +305,9 @@ let
tls_private_key.nixos-in-production = tls_private_key { … };
local_sensitive_file.ssh_key_file = ssh_key_file { … };
local_sensitive_file.ssh_private_key = local_sensitive_file { … };
local_file.ssh_public_key = local_file { … };
aws_key_pair.nixos-in-production = aws_key_pair { … };
Expand Down Expand Up @@ -352,7 +358,7 @@ resource "aws_security_group" "todo" {
}
```

The next three resources generate an SSH key pair that we'll use to manage the machine:
The next four resources generate an SSH key pair that we'll use to manage the machine:

```hcl
# Generate an SSH key pair as strings stored in Terraform state
Expand All @@ -362,11 +368,16 @@ resource "tls_private_key" "nixos-in-production" {
# Synchronize the SSH private key to a local file that the "nixos" module can
# use
resource "local_sensitive_file" "ssh_key_file" {
resource "local_sensitive_file" "ssh_private_key" {
filename = "${path.module}/id_ed25519"
content = tls_private_key.nixos-in-production.private_key_openssh
}
resource "local_file" "ssh_public_key" {
filename = "${path.module}/id_ed25519.pub"
content = tls_private_key.nixos-in-production.public_key_openssh
}
# Mirror the SSH public key to EC2 so that we can later install the public key
# as an authorized key for our server
resource "aws_key_pair" "nixos-in-production" {
Expand Down Expand Up @@ -399,6 +410,12 @@ resource "aws_instance" "todo" {
root_block_device {
volume_size = 7
}
# We will use this in a future chapter to bootstrap other secrets
user_data = <<-EOF
#!/bin/sh
(umask 377; echo '${tls_private_key.nixos-in-production.private_key_openssh}' > /var/lib/id_ed25519)
EOF
}
```

Expand Down Expand Up @@ -468,7 +485,9 @@ let
tls_private_key.nixos-in-production = tls_private_key { … };
local_sensitive_file.ssh_key_file = ssh_key_file { … };
local_sensitive_file.ssh_private_key = local_sensitive_file { … };
local_file.ssh_public_key = local_file { … };
aws_key_pair.nixos-in-production = aws_key_pair { … };
Expand Down Expand Up @@ -506,7 +525,7 @@ module "nixos" {
# Build our NixOS configuration on the same machine that we're deploying to
arguments = [ "--build-host", "root@${aws_instance.todo.public_ip}" ]
ssh_options = "-o StrictHostKeyChecking=accept-new -i ${local_sensitive_file.ssh_key_file.filename}"
ssh_options = "-o StrictHostKeyChecking=accept-new -i ${local_sensitive_file.ssh_private_key.filename}"
depends_on = [ null_resource.wait ]
}
Expand Down Expand Up @@ -687,8 +706,8 @@ Terraform will recreate this private key file locally for each developer that ma
```bash
Terraform will perform the following actions:
# local_sensitive_file.ssh_key_file will be created
+ resource "local_sensitive_file" "ssh_key_file" {
# local_sensitive_file.ssh_private_key will be created
+ resource "local_sensitive_file" "ssh_private_key" {
+ content = (sensitive value)
+ directory_permission = "0700"
+ file_permission = "0700"
Expand All @@ -700,3 +719,5 @@ Plan: 1 to add, 0 to change, 0 to destroy.
```
… indicating that Terraform will download the private key from the S3 backend and create a secure local copy in order to `ssh` into the machine.
However, it's completely fine to add the public key to version control if you want.
22 changes: 22 additions & 0 deletions templates/continuous-deployment/flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{ inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/22.11";

sops-nix.url = "github:Mic92/sops-nix/bd695cc4d0a5e1bead703cc1bec5fa3094820a81";
};

outputs = { nixpkgs, sops-nix, ... }: {
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";

modules = [ ./module.nix sops-nix.nixosModules.sops ];
};
};

nixConfig = {
extra-substituters = [ "https://cache.garnix.io" ];

extra-trusted-public-keys = [
"cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="
];
};
}
130 changes: 130 additions & 0 deletions templates/continuous-deployment/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
terraform {
required_version = ">= 1.3.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.56"
}
}
}

variable "region" {
type = string
nullable = false
}

provider "aws" {
profile = "nixos-in-production"
region = var.region
}

resource "aws_security_group" "todo" {
# The "nixos" Terraform module requires SSH access to the machine to deploy
# our desired NixOS configuration.
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [ "0.0.0.0/0" ]
}

# We will be building our NixOS configuration on the target machine, so we
# permit all outbound connections so that the build can download any missing
# dependencies.
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [ "0.0.0.0/0" ]
}

# Allow port 80 so that we can view our TODO list web page
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [ "0.0.0.0/0" ]
}
}

# Generate an SSH key pair as strings stored in Terraform state
resource "tls_private_key" "nixos-in-production" {
algorithm = "ED25519"
}

# Synchronize the SSH private key to a local file that the "nixos" module can
# use
resource "local_sensitive_file" "ssh_private_key" {
filename = "${path.module}/id_ed25519"
content = tls_private_key.nixos-in-production.private_key_openssh
}

resource "local_file" "ssh_public_key" {
filename = "${path.module}/id_ed25519.pub"
content = tls_private_key.nixos-in-production.public_key_openssh
}

# Mirror the SSH public key to EC2 so that we can later install the public key
# as an authorized key for our server
resource "aws_key_pair" "nixos-in-production" {
public_key = tls_private_key.nixos-in-production.public_key_openssh
}

module "ami" {
source = "github.com/Gabriella439/terraform-nixos-ng//ami?ref=d8563d06cc65bc699ffbf1ab8d692b1343ecd927"
release = "22.11"
region = var.region
system = "x86_64-linux"
}

resource "aws_instance" "todo" {
# This will be an AMI for a stock NixOS server which we'll get to below.
ami = module.ami.ami

# We could use a smaller instance size, but at the time of this writing the
# t3.micro instance type is available for 750 hours under the AWS free tier.
instance_type = "t3.micro"

# Install the security groups we defined earlier
security_groups = [ aws_security_group.todo.name ]

# Install our SSH public key as an authorized key
key_name = aws_key_pair.nixos-in-production.key_name

# Request a bit more space because we will be building on the machine
root_block_device {
volume_size = 7
}

# We will use this in a future chapter to bootstrap other secrets
user_data = <<-EOF
#!/bin/sh
(umask 377; echo '${tls_private_key.nixos-in-production.private_key_openssh}' > /var/lib/id_ed25519)
EOF
}

# This ensures that the instance is reachable via `ssh` before we deploy NixOS
resource "null_resource" "wait" {
provisioner "remote-exec" {
connection {
host = aws_instance.todo.public_dns
private_key = tls_private_key.nixos-in-production.private_key_openssh
}

inline = [ ":" ] # Do nothing; we're just testing SSH connectivity
}
}

module "nixos" {
source = "github.com/Gabriella439/terraform-nixos-ng//nixos?ref=d8563d06cc65bc699ffbf1ab8d692b1343ecd927"
host = "root@${aws_instance.todo.public_ip}"
flake = ".#default"
arguments = [ "--build-host", "root@${aws_instance.todo.public_ip}" ]
ssh_options = "-o StrictHostKeyChecking=accept-new -i ${local_sensitive_file.ssh_private_key.filename}"
depends_on = [ null_resource.wait ]
}

output "public_dns" {
value = aws_instance.todo.public_dns
}
45 changes: 45 additions & 0 deletions templates/continuous-deployment/module.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{ modulesPath, ... }:

{ imports = [ "${modulesPath}/virtualisation/amazon-image.nix" ];

documentation.enable = false;

services.nginx = {
enable = true;

virtualHosts.localhost.locations."/" = {
index = "index.html";

root = ./www;
};
};

networking.firewall.allowedTCPPorts = [ 80 ];

system.stateVersion = "22.11";

sops = {
defaultSopsFile = ./secrets.yaml;

age.sshKeyPaths = [ "/var/lib/id_ed25519" ];

secrets.github-access-token = { };
};

nix.extraOptions = "!include /run/secrets/github-access-token";

nix.settings.extra-experimental-features = [ "nix-command" "flakes" ];

system.autoUpgrade = {
enable = true;

# Replace ${username}/${repository} with your repository's address
flake = "github:${username}/${repository}#default";

# Poll the `main` branch for changes once a minute
dates = "minutely";

# You need this if you poll more than once an hour
flags = [ "--option" "tarball-ttl" "0" ];
};
}
23 changes: 23 additions & 0 deletions templates/continuous-deployment/www/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<html>
<body>
<button id='add'>+</button>
</body>
<script>
let add = document.getElementById('add');
function newTask() {
let subtract = document.createElement('button');
subtract.textContent = "-";
let input = document.createElement('input');
input.setAttribute('type', 'text');
let div = document.createElement('div');
div.replaceChildren(subtract, input);
function remove() {
div.replaceChildren();
div.remove();
}
subtract.addEventListener('click', remove);
add.before(div);
}
add.addEventListener('click', newTask);
</script>
</html>
15 changes: 13 additions & 2 deletions templates/terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,16 @@ resource "tls_private_key" "nixos-in-production" {

# Synchronize the SSH private key to a local file that the "nixos" module can
# use
resource "local_sensitive_file" "ssh_key_file" {
resource "local_sensitive_file" "ssh_private_key" {
filename = "${path.module}/id_ed25519"
content = tls_private_key.nixos-in-production.private_key_openssh
}

resource "local_file" "ssh_public_key" {
filename = "${path.module}/id_ed25519.pub"
content = tls_private_key.nixos-in-production.public_key_openssh
}

# Mirror the SSH public key to EC2 so that we can later install the public key
# as an authorized key for our server
resource "aws_key_pair" "nixos-in-production" {
Expand Down Expand Up @@ -91,6 +96,12 @@ resource "aws_instance" "todo" {
root_block_device {
volume_size = 7
}

# We will use this in a future chapter to bootstrap other secrets
user_data = <<-EOF
#!/bin/sh
(umask 377; echo '${tls_private_key.nixos-in-production.private_key_openssh}' > /var/lib/id_ed25519)
EOF
}

# This ensures that the instance is reachable via `ssh` before we deploy NixOS
Expand All @@ -110,7 +121,7 @@ module "nixos" {
host = "root@${aws_instance.todo.public_ip}"
flake = ".#default"
arguments = [ "--build-host", "root@${aws_instance.todo.public_ip}" ]
ssh_options = "-o StrictHostKeyChecking=accept-new -i ${local_sensitive_file.ssh_key_file.filename}"
ssh_options = "-o StrictHostKeyChecking=accept-new -i ${local_sensitive_file.ssh_private_key.filename}"
depends_on = [ null_resource.wait ]
}

Expand Down

0 comments on commit 5989c6b

Please sign in to comment.