diff --git a/packages/.gitignore b/packages/.gitignore index ef9dcfb..6ed06fc 100644 --- a/packages/.gitignore +++ b/packages/.gitignore @@ -39,3 +39,6 @@ testem.log Thumbs.db .nx/cache + +# Build logs +build.log diff --git a/packages/server/package.json b/packages/server/package.json index 549a755..8e839ad 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -7,7 +7,7 @@ "dev": "nodemon src/index.ts", "build": "tsc", "dev:build": "docker build -t contained-server:dev -f Dockerfile.dev .", - "dev:start": "docker run -v $(realpath $pwd ./node_modules):/usr/src/app/node_modules -v $(pwd):/usr/src/app -v /var/run/docker.sock:/var/run/docker.sock -d -p 3000:3000 contained-server:dev", + "dev:start": "docker run -v $(realpath $pwd ./node_modules):/usr/src/app/node_modules -v $(pwd):/usr/src/app -v /var/run/docker.sock:/var/run/docker.sock -p 3000:3000 contained-server:dev", "prod:build": "docker build --platform linux/amd64 -t ghcr.io/leoriviera/contained-server:latest .", "prod:push": "docker push ghcr.io/leoriviera/contained-server:latest" }, diff --git a/packages/ssh/Dockerfile b/packages/ssh/Dockerfile index 01ff40f..f3f788d 100644 --- a/packages/ssh/Dockerfile +++ b/packages/ssh/Dockerfile @@ -7,7 +7,7 @@ ENV BLOG_GITHUB_REPO=$BLOG_GITHUB_REPO LABEL com.centurylinklabs.watchtower.enable="false" # Install packages -RUN apt update && apt install openssh-server sudo curl unzip sl rsync neofetch -y +RUN apt update && apt install openssh-server sudo curl unzip sl rsync neofetch moreutils gettext-base -y # Create a user "sshuser" and group "sshgroup" RUN groupadd usergroup && useradd -ms /bin/bash -g usergroup user @@ -25,6 +25,9 @@ RUN echo 'eval "$(starship init bash)"' >> /home/user/.bashrc COPY --chown=root etc /tmp-etc RUN chmod 755 -R /tmp-etc/* +# As env vars aren't available in update-motd.d, replace env vars in etc/ files with envsubst +# Requires sponge from moreutils (see https://stackoverflow.com/a/74551579) +RUN find /tmp-etc -type f -exec sh -c "cat {} | envsubst | sponge {}" \; # Remove existing message of the day RUN rm /etc/update-motd.d/* @@ -41,4 +44,4 @@ RUN service ssh start # Expose docker port 22 EXPOSE 22 -CMD timeout 3600 /usr/sbin/sshd -o "SetEnv=BLOG_GITHUB_REPO=$BLOG_GITHUB_REPO" -D +CMD timeout 3600 /usr/sbin/sshd -D diff --git a/packages/ssh/etc/update-motd.d/00-header b/packages/ssh/etc/update-motd.d/00-header index f776829..b860d9f 100644 --- a/packages/ssh/etc/update-motd.d/00-header +++ b/packages/ssh/etc/update-motd.d/00-header @@ -3,16 +3,16 @@ # Running code to fetch the blog content in profile.d doesn't work, as # update-motd.d runs first. So, we'll just fetch the blog content here. # to make sure the latest post is always displayed. - curl -s -L -o /home/user/blog.zip "$BLOG_GITHUB_REPO/archive/main.zip" unzip -q /home/user/blog.zip -d /home/user mv /home/user/blog-main /home/user/blog rm /home/user/blog.zip +chown -R user /home/user/blog printf "Welcome to my Terminal blog. I'm still thinking of a catchy name for it.\n" # printf "If you've never used the terminal before, you can get started with [cat ~/help.md].\n" printf "(You can click on any commands in square brackets, by the way, to run them automatically!)\n" printf "\n" printf "Check out the latest post at [cat ~/blog/$(ls -t /home/user/blog | head -1)]!\n" -printf "\n"` +printf "\n" printf "Interested in hiring me? Check out my CV with [cat ~/cv/README.md].\n" diff --git a/packages/ssh/package.json b/packages/ssh/package.json index abfc10c..0e5673b 100644 --- a/packages/ssh/package.json +++ b/packages/ssh/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "A simple SSH server that runs in a container", "scripts": { - "dev:build": "docker build -t contained-ssh:dev .", + "dev:build": "docker build -t contained-ssh:dev . &> build.log", "prod:build": "docker build --platform linux/amd64 -t ghcr.io/leoriviera/contained-ssh:latest .", "prod:push": "docker push ghcr.io/leoriviera/contained-ssh:latest" } diff --git a/terraform/frontend/main.tf b/terraform/frontend/main.tf deleted file mode 100644 index f2702bf..0000000 --- a/terraform/frontend/main.tf +++ /dev/null @@ -1,7 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - } -} diff --git a/terraform/frontend/outputs.tf b/terraform/frontend/outputs.tf deleted file mode 100644 index 4dc2ffc..0000000 --- a/terraform/frontend/outputs.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "s3_bucket_name" { - value = aws_s3_bucket.bucket.bucket -} diff --git a/terraform/frontend/variables.tf b/terraform/frontend/variables.tf deleted file mode 100644 index d33a183..0000000 --- a/terraform/frontend/variables.tf +++ /dev/null @@ -1,23 +0,0 @@ -data "aws_cloudfront_cache_policy" "caching_policy" { - name = "Managed-CachingOptimized" -} - -variable "certificate_arn" { - type = string - nullable = false -} - -variable "hosted_zone_id" { - type = string - nullable = false -} - -variable "domain" { - type = string - nullable = false -} - -variable "iam_user_arn" { - type = string - nullable = false -} diff --git a/terraform/infrastructure/actions.tf b/terraform/infrastructure/actions.tf index 0403eec..09c26fd 100644 --- a/terraform/infrastructure/actions.tf +++ b/terraform/infrastructure/actions.tf @@ -21,7 +21,7 @@ resource "github_actions_environment_variable" "aws_s3_bucket_name" { environment = github_repository_environment.repo_env.environment repository = data.github_repository.repo.name variable_name = "AWS_S3_BUCKET_NAME" - value = var.s3_bucket_name + value = aws_s3_bucket.bucket.bucket } resource "github_actions_environment_variable" "aws_region" { diff --git a/terraform/frontend/cloudfront.tf b/terraform/infrastructure/cloudfront.tf similarity index 88% rename from terraform/frontend/cloudfront.tf rename to terraform/infrastructure/cloudfront.tf index c37ec5d..c5e9582 100644 --- a/terraform/frontend/cloudfront.tf +++ b/terraform/infrastructure/cloudfront.tf @@ -21,7 +21,7 @@ resource "aws_cloudfront_distribution" "cloudfront" { origin_access_control_id = aws_cloudfront_origin_access_control.s3_access.id } - aliases = [var.domain] + aliases = [var.frontend_domain] default_cache_behavior { compress = true @@ -42,7 +42,7 @@ resource "aws_cloudfront_distribution" "cloudfront" { } viewer_certificate { - acm_certificate_arn = var.certificate_arn + acm_certificate_arn = aws_acm_certificate_validation.certificate.certificate_arn ssl_support_method = "sni-only" } @@ -51,8 +51,8 @@ resource "aws_cloudfront_distribution" "cloudfront" { # Create a record to point the front-end subdomain at the CloudFront distribution resource "aws_route53_record" "frontend" { - zone_id = var.hosted_zone_id - name = "${var.domain}." + zone_id = data.aws_route53_zone.hosted_zone.zone_id + name = "${var.frontend_domain}." type = "A" alias { diff --git a/terraform/infrastructure/iam.tf b/terraform/infrastructure/iam.tf index 666d428..1ea8ccd 100644 --- a/terraform/infrastructure/iam.tf +++ b/terraform/infrastructure/iam.tf @@ -15,17 +15,16 @@ resource "aws_iam_policy" "s3_access_policy" { Action = [ "s3:GetObject", "s3:DeleteObject", - "s3:PutObject", - "s3:ListBucket" + "s3:PutObject" ] - Resource = "arn:aws:s3:::${var.s3_bucket_name}/*" + Resource = "${aws_s3_bucket.bucket.arn}/*" } ] }) } # Attach IAM policy to user -resource "aws_iam_user_policy_attachment" "test-attach" { +resource "aws_iam_user_policy_attachment" "s3_user_access" { user = aws_iam_user.gh_action.name policy_arn = aws_iam_policy.s3_access_policy.arn } @@ -34,3 +33,27 @@ resource "aws_iam_user_policy_attachment" "test-attach" { resource "aws_iam_access_key" "gh_action" { user = aws_iam_user.gh_action.name } + +data aws_iam_policy_document "ec2_assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "ec2_iam_role" { + assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json +} + +resource "aws_iam_role_policy_attachment" "s3_user_access" { + role = aws_iam_role.ec2_iam_role.name + policy_arn = aws_iam_policy.s3_access_policy.arn +} + +resource "aws_iam_instance_profile" "instance_profile" { + role = aws_iam_role.ec2_iam_role.name +} diff --git a/terraform/infrastructure/outputs.tf b/terraform/infrastructure/outputs.tf index baee9ce..8d4b11a 100644 --- a/terraform/infrastructure/outputs.tf +++ b/terraform/infrastructure/outputs.tf @@ -1,3 +1,7 @@ +output "availability_zones" { + value = local.availability_zones +} + output "certificate_arn" { value = aws_acm_certificate_validation.certificate.certificate_arn } @@ -6,12 +10,8 @@ output "hosted_zone_id" { value = data.aws_route53_zone.hosted_zone.zone_id } -output "iam_user_arn" { - value = aws_iam_user.gh_action.arn -} - -output "availability_zones" { - value = local.availability_zones +output "instance_profile_arn" { + value = aws_iam_instance_profile.instance_profile.arn } output "vpc_id" { @@ -21,3 +21,7 @@ output "vpc_id" { output "vpc_subnets" { value = [for subnet in aws_subnet.public_subnets : subnet.id] } + +output "s3_bucket_name" { + value = aws_s3_bucket.bucket.bucket +} diff --git a/terraform/frontend/s3.tf b/terraform/infrastructure/s3.tf similarity index 66% rename from terraform/frontend/s3.tf rename to terraform/infrastructure/s3.tf index f8b6355..24704d6 100644 --- a/terraform/frontend/s3.tf +++ b/terraform/infrastructure/s3.tf @@ -3,7 +3,7 @@ resource "aws_s3_bucket" "bucket" { force_destroy = true } -# Allow access to the S3 bucket through CloudFront +# Allow access to the S3 bucket through CloudFront, GitHub Actions and EC2 resource "aws_s3_bucket_policy" "allow_cloudfront_access" { bucket = aws_s3_bucket.bucket.id policy = jsonencode({ @@ -25,7 +25,7 @@ resource "aws_s3_bucket_policy" "allow_cloudfront_access" { } }, { - Sid = "AllowS3BucketAccess", + Sid = "AllowGitHubS3BucketAccess", Effect = "Allow", Action = [ "s3:GetObject", @@ -35,12 +35,12 @@ resource "aws_s3_bucket_policy" "allow_cloudfront_access" { Resource = "${aws_s3_bucket.bucket.arn}/*", Principal = { AWS = [ - var.iam_user_arn + aws_iam_user.gh_action.arn ] } }, { - Sid = "AllowS3BucketAccess", + Sid = "AllowGitHubS3BucketAccess", Effect = "Allow", Action = [ "s3:ListBucket" @@ -48,7 +48,22 @@ resource "aws_s3_bucket_policy" "allow_cloudfront_access" { Resource = "${aws_s3_bucket.bucket.arn}", Principal = { AWS = [ - var.iam_user_arn + aws_iam_user.gh_action.arn + ] + } + }, + { + Sid = "AllowEC2S3BucketAccess", + Effect = "Allow", + Action = [ + "s3:GetObject", + "s3:DeleteObject", + "s3:PutObject" + ] + Resource = "${aws_s3_bucket.bucket.arn}/*", + Principal = { + AWS = [ + aws_iam_role.ec2_iam_role.arn ] } } diff --git a/terraform/infrastructure/variables.tf b/terraform/infrastructure/variables.tf index f393643..412a39d 100644 --- a/terraform/infrastructure/variables.tf +++ b/terraform/infrastructure/variables.tf @@ -1,5 +1,9 @@ data "aws_availability_zones" "all" {} +data "aws_cloudfront_cache_policy" "caching_policy" { + name = "Managed-CachingOptimized" +} + data "aws_ec2_instance_type_offerings" "available" { for_each = toset(data.aws_availability_zones.all.names) @@ -17,15 +21,15 @@ data "aws_ec2_instance_type_offerings" "available" { location_type = "availability-zone" } +data "github_repository" "repo" { + full_name = var.repository +} + data "aws_route53_zone" "hosted_zone" { name = "${var.domain}." private_zone = false } -data "github_repository" "repo" { - full_name = var.repository -} - # Finding AZs which support particular instances # https://stackoverflow.com/a/63728735 locals { @@ -55,11 +59,6 @@ variable "server_domain" { nullable = false } -variable "s3_bucket_name" { - type = string - nullable = false -} - variable "aws_region" { type = string nullable = false diff --git a/terraform/main.tf b/terraform/main.tf index f714bc4..3ff0c16 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -6,7 +6,6 @@ module "infrastructure" { frontend_domain = "${var.frontend_subdomain}.${var.domain}" instance_type = var.instance_type repository = var.repository - s3_bucket_name = module.frontend.s3_bucket_name server_domain = "${var.server_subdomain}.${var.domain}" providers = { @@ -23,15 +22,9 @@ module "server" { domain = "${var.server_subdomain}.${var.domain}" hosted_zone_id = module.infrastructure.hosted_zone_id instance_type = var.instance_type + instance_profile_arn = module.infrastructure.instance_profile_arn + s3_bucket_name = module.infrastructure.s3_bucket_name vpc_id = module.infrastructure.vpc_id vpc_subnets = module.infrastructure.vpc_subnets } -module "frontend" { - source = "./frontend" - - certificate_arn = module.infrastructure.certificate_arn - domain = "${var.frontend_subdomain}.${var.domain}" - hosted_zone_id = module.infrastructure.hosted_zone_id - iam_user_arn = module.infrastructure.iam_user_arn -} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index bd59d97..aea2d63 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -7,5 +7,5 @@ output "server_subdomain" { } output "s3_bucket_name" { - value = module.frontend.s3_bucket_name + value = module.infrastructure.s3_bucket_name } diff --git a/terraform/server/asg.tf b/terraform/server/asg.tf index d5eb8d2..aee6432 100644 --- a/terraform/server/asg.tf +++ b/terraform/server/asg.tf @@ -32,7 +32,12 @@ resource "aws_launch_template" "launch_template" { image_id = tolist(data.aws_ami_ids.amazon_linux.ids)[0] instance_type = var.instance_type vpc_security_group_ids = [aws_security_group.scaling_group_sg.id] - user_data = filebase64("${path.module}/user-data.sh") + user_data = base64encode(templatefile("${path.module}/user-data.sh", { + bucket_name = var.s3_bucket_name + })) + iam_instance_profile { + arn = var.instance_profile_arn + } update_default_version = true block_device_mappings { device_name = "/dev/xvda" diff --git a/terraform/server/docker-compose.yml b/terraform/server/docker-compose.yml new file mode 100644 index 0000000..ae52fec --- /dev/null +++ b/terraform/server/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3" + +services: + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 30 + platform: linux/amd64 + server: + image: ghcr.io/leoriviera/contained-server:latest + ports: + - "3000:3000" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + restart: always + platform: linux/amd64 diff --git a/terraform/server/ec2.tf b/terraform/server/ec2.tf new file mode 100644 index 0000000..84267cc --- /dev/null +++ b/terraform/server/ec2.tf @@ -0,0 +1,27 @@ +# Upload docker-compose.yml to S3 +resource "aws_s3_object" "docker_compose" { + bucket = var.s3_bucket_name + key = "config/docker-compose.yml" + source = "${path.module}/docker-compose.yml" + etag = filemd5("${path.module}/docker-compose.yml") +} + + + +# data aws_iam_policy_document "s3_read_access" { +# statement { +# actions = ["s3:Get*", "s3:List*"] +# resources = ["arn:aws:s3:::*"] +# } +# } + + +# resource "aws_iam_role_policy" "s3_read_access" { +# role = aws_iam_role.ec2_iam_role.name +# policy = data.aws_iam_policy_document.s3_read_access.json +# } + +# resource "aws_iam_role_policy_attachment" "s3_read_access" { +# role = aws_iam_role.ec2_iam_role.name +# policy_arn =aws_iam_policy.s3_read_access.arn +# } diff --git a/terraform/server/user-data.sh b/terraform/server/user-data.sh index bc1635d..24cc8a2 100644 --- a/terraform/server/user-data.sh +++ b/terraform/server/user-data.sh @@ -1,8 +1,15 @@ #!/bin/bash sudo yum update -sudo yum install docker -y +sudo yum install docker containerd git screen -y +wget https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) +sudo mv docker-compose-$(uname -s)-$(uname -m) /usr/libexec/docker/cli-plugins/docker-compose +chmod +x /usr/libexec/docker/cli-plugins/docker-compose + sudo systemctl enable docker.service sudo service docker start sudo usermod -a -G docker ec2-user newgrp docker -docker run --pull always --restart always --platform linux/amd64 -v /var/run/docker.sock:/var/run/docker.sock -p 3000:3000 -d ghcr.io/leoriviera/contained-server:latest + +aws s3 cp s3://${bucket_name}/config/docker-compose.yml /home/ec2-user/docker-compose.yml +chown ec2-user:ec2-user /home/ec2-user/docker-compose.yml +docker compose -f /home/ec2-user/docker-compose.yml up -d diff --git a/terraform/server/variables.tf b/terraform/server/variables.tf index a218a21..eeccfa6 100644 --- a/terraform/server/variables.tf +++ b/terraform/server/variables.tf @@ -1,21 +1,3 @@ -# Find public subnets in the default VPC -# data "aws_subnets" "subnets" { -# filter { -# name = "vpc-id" -# values = [var.vpc_id] -# } - -# filter { -# name = "map-public-ip-on-launch" -# values = [true] -# } -# } - -# data "aws_subnet" "available" { -# for_each = toset(data.aws_subnets.subnets.ids) -# id = each.key -# } - data "aws_ami_ids" "amazon_linux" { owners = ["amazon"] @@ -63,3 +45,13 @@ variable "vpc_subnets" { type = list(string) nullable = false } + +variable "s3_bucket_name" { + type = string + nullable = false +} + +variable "instance_profile_arn" { + type = string + nullable = false +}