<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://adminforth.dev/blog/</id>
    <title>Vue &amp; Node admin panel framework Blog</title>
    <updated>2025-11-04T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://adminforth.dev/blog/"/>
    <subtitle>Vue &amp; Node admin panel framework Blog</subtitle>
    <icon>https://adminforth.dev/img/favicon.png</icon>
    <entry>
        <title type="html"><![CDATA[IaC Simplified: K3s on EC2 Deployments with Terraform, Helm, Ansible & Amazon ECR]]></title>
        <id>https://adminforth.dev/blog/k3s-ec2-deployment/</id>
        <link href="https://adminforth.dev/blog/k3s-ec2-deployment/"/>
        <updated>2025-11-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The ultimate step-by-step guide to cost-effective, build-time-efficient, and easy managable EC2 deployments using K3s, Terraform, Helm, Ansible, and a Amazon ECR registry.]]></summary>
        <content type="html"><![CDATA[<p>This guide shows how to deploy own Docker apps (with AdminForth as example) to Amazon EC2 instance with K3s and Terraform involving pushing images into Amazon ECR.</p>
<p>Needed resources:</p>
<ul>
<li class="">AWS account where we will auto-spawn EC2 instance. We will use <code>t3a.small</code> instance (2 vCPUs, 2GB RAM) which costs <code>~14$</code> per month in <code>us-west-2</code> region (cheapest region). Also it will take <code>$2</code> per month for EBS gp2 storage (20GB) for EC2 instance.</li>
<li class="">Also AWS ECR will charge for <code>$0.09</code> per GB of data egress traffic (from EC2 to the internet) - this needed to load docker build cache.</li>
</ul>
<p>The setup shape:</p>
<ul>
<li class="">Build is done using IaaC approach with HashiCorp Terraform, so almoast no manual actions are needed from you. Every resource including EC2 server instance is described in code which is commited to repo.</li>
<li class="">Docker images and build cache are stored on Amazon ECR</li>
<li class="">Total build time for average commit to AdminForth app (with Vite rebuilds) is around 3 minutes.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-exactly-k3s">Why exactly K3s?<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#why-exactly-k3s" class="hash-link" aria-label="Direct link to Why exactly K3s?" title="Direct link to Why exactly K3s?" translate="no">​</a></h2>
<p>Previously, our blog featured posts about different types of application deployment, but without the use of Kubernetes. This post will look at the cheapest option for deploying an application using k3s (a lightweight version of k8s Kubernetes). This option is more interesting than most of the alternatives, primarily because of its automation and scalability (it is, of course, inferior to the “older” K8s, but it also requires significantly fewer resources).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-we-will-store-containers">How we will store containers?<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#how-we-will-store-containers" class="hash-link" aria-label="Direct link to How we will store containers?" title="Direct link to How we will store containers?" translate="no">​</a></h2>
<p>The ECR repository will be used for storage. Since we are working with AWS, this is the most reliable option. The image must be assembled from local files on the machine and then sent to the Amazon server. All instructions for performing these actions will be provided below.</p>
<h1>Prerequisites</h1>
<p>I will assume you run Ubuntu (Native or WSL2).</p>
<p>You should have terraform, here is official repository:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">sudo apt update &amp;&amp; sudo apt install terraform</span><br></span></code></pre></div></div>
<p>AWS CLI:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> snap </span><span class="token function" style="color:#e6db74">install</span><span class="token plain"> aws-cli </span><span class="token parameter variable" style="color:#f8f8f2">--classic</span><br></span></code></pre></div></div>
<p>HELM:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-fsSL</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-o</span><span class="token plain"> get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">chmod</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">700</span><span class="token plain"> get_helm.sh</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">./get_helm.sh</span><br></span></code></pre></div></div>
<p>Also you need Doker Daemon running. We recommend Docker Desktop running. ON WSL2 make sure you have Docker Desktop WSL2 integration enabled.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">docker</span><span class="token plain"> version</span><br></span></code></pre></div></div>
<p>It is also worth having <code>kubectl</code> locally on your machine for more convenient interaction with nodes and pods, but this is not mandatory.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-LO</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"https://dl.k8s.io/release/</span><span class="token string variable" style="color:#f8f8f2">$(</span><span class="token string variable function" style="color:#e6db74">curl</span><span class="token string variable" style="color:#f8f8f2"> </span><span class="token string variable parameter variable" style="color:#f8f8f2">-L</span><span class="token string variable" style="color:#f8f8f2"> </span><span class="token string variable parameter variable" style="color:#f8f8f2">-s</span><span class="token string variable" style="color:#f8f8f2"> https://dl.k8s.io/release/stable.txt</span><span class="token string variable" style="color:#f8f8f2">)</span><span class="token string" style="color:#a6e22e">/bin/linux/amd64/kubectl"</span><br></span></code></pre></div></div>
<p>Last step is download ansible:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> add-apt-repository </span><span class="token parameter variable" style="color:#f8f8f2">--yes</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">--update</span><span class="token plain"> ppa:ansible/ansible</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">apt</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">install</span><span class="token plain"> ansible </span><span class="token parameter variable" style="color:#f8f8f2">-y</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<h1>Practice - deploy setup</h1>
<p>Assume you have your AdminForth project in <code>myadmin</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1---create-a-ssh-keypair">Step 1 - create a SSH keypair<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#step-1---create-a-ssh-keypair" class="hash-link" aria-label="Direct link to Step 1 - create a SSH keypair" title="Direct link to Step 1 - create a SSH keypair" translate="no">​</a></h2>
<p>Make sure you are still in <code>deploy</code> folder, run next command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">mkdir</span><span class="token plain"> .keys </span><span class="token operator" style="color:#66d9ef">&amp;&amp;</span><span class="token plain"> ssh-keygen </span><span class="token parameter variable" style="color:#f8f8f2">-f</span><span class="token plain"> .keys/id_rsa </span><span class="token parameter variable" style="color:#f8f8f2">-N</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">""</span><br></span></code></pre></div></div>
<p>Now it should create <code>deploy/.keys/id_rsa</code> and <code>deploy/.keys/id_rsa.pub</code> files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2---gitignore-file">Step 2 - .gitignore file<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#step-2---gitignore-file" class="hash-link" aria-label="Direct link to Step 2 - .gitignore file" title="Direct link to Step 2 - .gitignore file" translate="no">​</a></h2>
<p>Create <code>deploy/.gitignore</code> file with next content:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">.terraform/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.keys/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfstate</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfstate.*</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfvars</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">tfplan</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">session-manager-plugin.deb</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.terraform.lock.hcl</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3---terraform-folder">Step 3 - Terraform folder<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#step-3---terraform-folder" class="hash-link" aria-label="Direct link to Step 3 - Terraform folder" title="Direct link to Step 3 - Terraform folder" translate="no">​</a></h2>
<p>First of all install Terraform as described here <a href="https://developer.hashicorp.com/terraform/install#linux" target="_blank" rel="noopener noreferrer" class="">terraform installation</a>.</p>
<p>After this create folder ../deploy/terraform</p>
<p>Create file <code>main.tf</code> in <code>deploy/terraform</code> folder:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/terraform/main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  required_providers {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    aws = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      source  = "hashicorp/aws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      version = "~&gt; 5.0"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">locals {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  aws_region           = "us-west-2"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_cidr             = "10.0.0.0/16"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_a_cidr        = "10.0.10.0/24"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_b_cidr        = "10.0.11.0/24"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  az_a                 = "us-west-2a"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  az_b                 = "us-west-2b"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app_name             = &lt;your_app_name&gt;</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app_source_code_path = "../../"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ansible_dir          = "../ansible/playbooks"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app_files            = fileset(local.app_source_code_path, "**")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  image_tag = sha256(join("", [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    for f in local.app_files :</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    try(filesha256("${local.app_source_code_path}/${f}"), "")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    if length(regexall("^deploy/", f)) == 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    &amp;&amp; length(regexall("^\\.vscode/", f)) == 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    &amp;&amp; length(regexall("^node_modules/", f)) == 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    &amp;&amp; length(regexall("^\\.gitignore", f)) == 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ]))</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress_ports = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    { from = 22, to = 22, protocol = "tcp", desc = "SSH" },</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    { from = 80, to = 80, protocol = "tcp", desc = "App HTTP (Traefik)" },</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    { from = 443, to = 443, protocol = "tcp", desc = "App HTTPS (Traefik)" },</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    { from = 6443, to = 6443, protocol = "tcp", desc = "Kubernetes API" }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">provider "aws" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  region  = local.aws_region</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  profile = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_ami" "ubuntu_22_04" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  most_recent = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  owners      = ["099720109477"] # Canonical ubuntu account ID</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "name"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_key_pair" "app_deployer" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name   = "terraform-deploy_${local.app_name}-key"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  public_key = file("../.keys/id_rsa.pub") # Path to your public SSH key</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_instance" "ec2_instance" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  instance_type = "t3a.small"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ami           = data.aws_ami.ubuntu_22_04.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  iam_instance_profile = aws_iam_instance_profile.instance_profile.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_id                   = aws_subnet.public_a.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_security_group_ids      = [aws_security_group.app_sg.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  associate_public_ip_address = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name                    = aws_key_pair.app_deployer.key_name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Name = local.app_name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    null_resource.docker_build_and_push</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # prevent accidental termination of ec2 instance and data loss</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  lifecycle {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    create_before_destroy = true #uncomment in production</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    #prevent_destroy       = true       #uncomment in production</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ignore_changes = [ami]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    replace_triggered_by = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      null_resource.docker_build_and_push</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  root_block_device {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_size = 10 // Size in GB for root partition</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_type = "gp2"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Even if the instance is terminated, the volume will not be deleted, delete it manually if needed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    delete_on_termination = true #change to false in production if data persistence is needed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "local_file" "ansible_inventory" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  content = &lt;&lt;EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">[k3s_nodes]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">${aws_instance.ec2_instance.public_ip} ansible_user=ubuntu ansible_ssh_private_key_file=../.keys/id_rsa</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filename = "../ansible/inventory.ini"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "wait_ssh" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_instance.ec2_instance]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = (</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      instance_id = aws_instance.ec2_instance.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  )</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = &lt;&lt;EOT</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    bash -c '</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    for i in {1..10}; do</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      nc -zv ${aws_instance.ec2_instance.public_ip} 22 &amp;&amp; echo "SSH is ready!" &amp;&amp; exit 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sleep 5</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    done</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    exit 1</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    '</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    EOT</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "ansible_provision" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    aws_instance.ec2_instance,</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    local_file.ansible_inventory,</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    null_resource.wait_ssh,</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    local_file.image_tag</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    instance_id = aws_instance.ec2_instance.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    interpreter = ["/bin/bash", "-c"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = &lt;&lt;-EOT</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      set -e</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ANSIBLE_HOST_KEY_CHECKING=False ansible-galaxy collection install community.kubernetes</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i ${path.module}/../ansible/inventory.ini ${local.ansible_dir}/playbook.yaml </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    EOT</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Replace <code>&lt;your_app_name&gt;</code> with your app name (no spaces, only underscores or letters)</p>
</blockquote>
<p>We will also need a file <code>container.tf</code></p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/terraform/container.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_ecr_repository" "app_repo" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name = local.app_name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  image_tag_mutability = "MUTABLE"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  image_scanning_configuration {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    scan_on_push = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  force_delete = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_caller_identity" "current" {}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "docker_build_and_push" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_ecr_repository.app_repo]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    image_tag = local.image_tag</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = &lt;&lt;-EOT</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      set -e</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      unset DOCKER_HOST</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      REPO_URL="${aws_ecr_repository.app_repo.repository_url}"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ACCOUNT_ID="${data.aws_caller_identity.current.account_id}"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      REGION="${local.aws_region}"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      TAG="${local.image_tag}"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "LOG: Logging in to ECR..."</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      aws ecr get-login-password --region $${REGION} | docker login --username AWS --password-stdin $${ACCOUNT_ID}.dkr.ecr.$${REGION}.amazonaws.com</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "LOG: Building Docker image..."</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker build --pull -t $${REPO_URL}:$${TAG} ${local.app_source_code_path}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "LOG: Pushing image to ECR..."</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker push $${REPO_URL}:$${TAG}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "LOG: Build and push complete. TAG=$${TAG}"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    EOT</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    interpreter = ["/bin/bash", "-c"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "local_file" "image_tag" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [null_resource.docker_build_and_push]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  content    = local.image_tag</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filename   = "${path.module}/image_tag.txt"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span></code></pre></div></div>
<p>This file contains a script that builds the Docker image locally. This is done for more flexible deployment. When changing the program code, there is no need to manually update the image on EC2 or in the repository. It is updated automatically with each terraform apply. Below is a table showing the time it takes to build this image from scratch and with minimal changes.</p>
<table><thead><tr><th>Feature</th><th>Time</th></tr></thead><tbody><tr><td>Initial build time*</td><td>0m45.445s</td></tr><tr><td>Rebuild time (changed index.ts)*</td><td>0m26.757s</td></tr></tbody></table>
<sub>* All tests done from local machine (Intel(R) Core(TM) i7 9760H, Docker Desktop/Ubuntu 32 GB RAM, 300Mbps up/down) up to working state</sub>
<p>Also, <code>resvpc.tf</code></p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/terraform/resvpc.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_vpc" "main" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  cidr_block = local.vpc_cidr</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  enable_dns_support   = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  enable_dns_hostnames = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = { Name = "main-vpc" }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_subnet" "public_a" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id                  = aws_vpc.main.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  cidr_block              = local.subnet_a_cidr</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  map_public_ip_on_launch = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  availability_zone       = local.az_a</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Name = "public-a"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_subnet" "public_b" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id                  = aws_vpc.main.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  cidr_block              = local.subnet_b_cidr</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  map_public_ip_on_launch = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  availability_zone       = local.az_b</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Name = "public-b"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_internet_gateway" "igw" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id = aws_vpc.main.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags   = { Name = "main-igw" }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_route_table" "public_rt" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id = aws_vpc.main.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  route {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_block = "0.0.0.0/0"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    gateway_id = aws_internet_gateway.igw.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = { Name = "public-rt" }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_route_table_association" "public_a_assoc" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_id      = aws_subnet.public_a.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  route_table_id = aws_route_table.public_rt.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_route_table_association" "public_b_assoc" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_id      = aws_subnet.public_b.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  route_table_id = aws_route_table.public_rt.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_security_group" "app_sg" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name   = "${local.app_name}-SecurityGroup"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id = aws_vpc.main.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  dynamic "ingress" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    for_each = local.ingress_ports</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    content {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      from_port   = ingress.value.from</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      to_port     = ingress.value.to</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      protocol    = ingress.value.protocol</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      description = ingress.value.desc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  egress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_role" "node_role" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name = "${local.app_name}node-role"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  assume_role_policy = jsonencode({</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Version = "2012-10-17"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Statement = [{</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      Effect    = "Allow"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      Principal = { Service = "ec2.amazonaws.com" }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      Action    = "sts:AssumeRole"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  })</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_role_policy_attachment" "ecr_read_only_attach" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  role       = aws_iam_role.node_role.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_role_policy_attachment" "ssm_core_policy" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  role       = aws_iam_role.node_role.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_instance_profile" "instance_profile" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name = "${local.app_name}-instance-profile"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  role = aws_iam_role.node_role.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span></code></pre></div></div>
<p>And <code>outputs.tf</code></p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/terraform/outputs.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">output "app_endpoint" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  value = "http://${aws_instance.ec2_instance.public_dns}"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">output "ssh_connect_command" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  value = "ssh -i .keys/id_rsa ubuntu@${aws_instance.ec2_instance.public_dns}"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4---helm">Step 4 - Helm<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#step-4---helm" class="hash-link" aria-label="Direct link to Step 4 - Helm" title="Direct link to Step 4 - Helm" translate="no">​</a></h3>
<p><strong>Helm</strong> is a command-line tool and a set of libraries that helps manage applications in Kubernetes.</p>
<p><strong>Helm Chart (Chart)</strong> is a package containing everything needed to run an application in Kubernetes. It's the equivalent of <code>apt</code> or <code>yum</code> packages in Linux.</p>
<p>A chart has this structure:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">helm_charts/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">├── Chart.yaml        # Metadata about the chart (name, version)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">├── values.yaml       # Default values (configuration)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">└── templates/        # Folder with Kubernetes templates (YAML files)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ├── deployment.yaml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ├── service.yaml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ├── ingress.yaml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    └── ...</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5---provider-helm">Step 5 - Provider Helm<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#step-5---provider-helm" class="hash-link" aria-label="Direct link to Step 5 - Provider Helm" title="Direct link to Step 5 - Provider Helm" translate="no">​</a></h3>
<p>Now we need to create .../deploy/helm and .../deploy/helm/helm_charts folders</p>
<p>you need to create a file <code>Chart.yaml</code> in it</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/helm/helm_charts/Chart.yaml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">apiVersion</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> v2</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> myadmink3s </span><span class="token comment" style="color:#8292a2;font-style:italic"># SET YOUR APP NAME</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">description</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Helm chart for myadmin app</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">version</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> 0.1.0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">appVersion</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"1.0.0"</span><br></span></code></pre></div></div>
<p>And <code>values.yaml</code></p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/helm/helm_charts/values.yaml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">appName</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> myadmink3s </span><span class="token comment" style="color:#8292a2;font-style:italic"># SET YOUR APP NAME LIKE IN Chart.yaml</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">appNameSpace</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> myadmin </span><span class="token comment" style="color:#8292a2;font-style:italic"># SET YOUR APP NAMESPACE</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">containerPort</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">3500</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">servicePort</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">80</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">adminSecret</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"your_secret"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">ecrImageFull</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">""</span><br></span></code></pre></div></div>
<p>After this create .../deploy/helm/helm_charts/templates folder</p>
<p>And create files here:</p>
<p><code>deployment.yaml</code></p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/helm/helm_charts/templates/deployment.yaml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">apiVersion</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> apps/v1</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">kind</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Deployment</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">metadata</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appName </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">deployment</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">namespace</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appNameSpace </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">spec</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">replicas</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">selector</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">matchLabels</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">app</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appName </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">template</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">metadata</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">labels</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">app</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appName </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">spec</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">containers</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appName </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"{{ .Values.ecrImageFull }}"</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">containerPort</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.containerPort </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"ADMINFORTH_SECRET"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">value</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"{{ .Values.adminSecret }}"</span><br></span></code></pre></div></div>
<p><code>ingress.yaml</code></p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/helm/helm_charts/templates/ingress.yaml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">apiVersion</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> networking.k8s.io/v1</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">kind</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Ingress</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">metadata</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appName </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">ingress</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">namespace</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appNameSpace </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">spec</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">rules</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">http</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">paths</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">path</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> /</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">pathType</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Prefix</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">backend</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">service</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appName </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">service</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token key atrule">port</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">              </span><span class="token key atrule">number</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.servicePort </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>And <code>service.yaml</code></p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/helm/helm_charts/templates/service.yaml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">apiVersion</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> v1</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">kind</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Service</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">metadata</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appName </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">service</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">namespace</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appNameSpace </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">spec</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">type</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> ClusterIP</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">selector</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">app</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.appName </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">port</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.servicePort </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">targetPort</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> .Values.containerPort </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>The comments in the <code>values.yaml</code> and <code>Chart.yaml</code> files indicate the names of the variables that need to be replaced. They must correspond to the variables in Ansible, which will be discussed later.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6---ansible">Step 6 - Ansible<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#step-6---ansible" class="hash-link" aria-label="Direct link to Step 6 - Ansible" title="Direct link to Step 6 - Ansible" translate="no">​</a></h3>
<p>If we explain the logic of deployment, Ansible plays a very important role here. If Terraform is used exclusively to configure cloud infrastructure in AWS, Ansible prepares it for the deployment of a Kubernetes cluster. Ansible Playbooks, in simple terms, are templates for configuring the system (of course, their functionality is much broader, but in this deployment method and in most cases, this is how they are used). That is, after preparing the instance with Terraform, it launches Ansible, which prepares it for the deployment of the Kubernetes cluster, after which it launches Helm.
This method allows for the highest quality deployment, because each stage is handled by software specialized for that particular stage.</p>
<p>So, create <code>/deploy/ansible/playbooks</code> folder</p>
<p>Then the file <code>playbook.yaml</code></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">/deploy/ansible/playbooks/playbook.yaml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">---</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">- name: Deploy application</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  hosts: k3s_nodes</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  become: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vars:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    k3s_version: </span><span class="token string" style="color:#a6e22e">"v1.27.3+k3s1"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    helm_version: </span><span class="token string" style="color:#a6e22e">"v3.12.3"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    kubeconfig_path: /etc/rancher/k3s/k3s.yaml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    helm_url: https://get.helm.sh/helm-v3.12.3-linux-amd64.tar.gz</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    helm_dest: /usr/local/bin/helm</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    app_name: myadmink3s    </span><span class="token comment" style="color:#8292a2;font-style:italic"># &lt;-- CHANGE TO YOUR APP NAME LIKE IN main.tf local.app_name</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    app_namespace: myadmin  </span><span class="token comment" style="color:#8292a2;font-style:italic"># &lt;-- CHANGE TO YOUR APP NAMESPACE</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    aws_account_id: </span><span class="token string" style="color:#a6e22e">"735356255780"</span><span class="token plain"> </span><span class="token comment" style="color:#8292a2;font-style:italic"># &lt;-- CHANGE TO YOUR AWS ACCOUNT ID</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    chart_path: /home/ubuntu/</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">app_name</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">/helm_charts</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tasks:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Read Docker image tag </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">local</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.set_fact:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        image_tag: </span><span class="token string" style="color:#a6e22e">"{{ lookup('file', '../../terraform/image_tag.txt') }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      delegate_to: localhost</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Install </span><span class="token function" style="color:#e6db74">unzip</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.apt:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        name: </span><span class="token function" style="color:#e6db74">unzip</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        state: present</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        update_cache: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Download AWS CLI v2</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.get_url:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        url: </span><span class="token string" style="color:#a6e22e">"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: /tmp/awscliv2.zip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        mode: </span><span class="token string" style="color:#a6e22e">'0644'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Unzip AWS CLI</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.unarchive:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        src: /tmp/awscliv2.zip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: /tmp</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        remote_src: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Install AWS CLI</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.command: /tmp/aws/install </span><span class="token parameter variable" style="color:#f8f8f2">--update</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      args:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        creates: /usr/local/bin/aws</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Update </span><span class="token function" style="color:#e6db74">apt</span><span class="token plain"> cache</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.apt:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        update_cache: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Install required packages</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.apt:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        name:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          - </span><span class="token function" style="color:#e6db74">curl</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          - </span><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          - software-properties-common</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          - apt-transport-https</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          - ca-certificates</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        state: present</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Download k3s installation script</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.get_url:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        url: https://get.k3s.io</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: /tmp/install_k3s.sh</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        mode: </span><span class="token string" style="color:#a6e22e">'0700'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Install k3s</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.command: /tmp/install_k3s.sh</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      environment:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        INSTALL_K3S_VERSION: </span><span class="token string" style="color:#a6e22e">"{{ k3s_version }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      args:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        creates: /usr/local/bin/k3s</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Get ECR token</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.command: aws ecr get-login-password </span><span class="token parameter variable" style="color:#f8f8f2">--region</span><span class="token plain"> us-west-2</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      register: ecr_token</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      changed_when: </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Configure K3s registry </span><span class="token keyword" style="color:#66d9ef">for</span><span class="token plain"> ECR</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.copy:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: /etc/rancher/k3s/registries.yaml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        content: </span><span class="token operator" style="color:#66d9ef">|</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          configs:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token string" style="color:#a6e22e">"735356255780.dkr.ecr.us-west-2.amazonaws.com"</span><span class="token builtin class-name" style="color:#e6db74">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">              auth:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">                username: AWS</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">                password: </span><span class="token string" style="color:#a6e22e">"{{ ecr_token.stdout }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        mode: </span><span class="token string" style="color:#a6e22e">'0600'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Restart k3s to apply registry changes</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.systemd:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        name: k3s</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        state: restarted</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        enabled: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Wait </span><span class="token keyword" style="color:#66d9ef">for</span><span class="token plain"> k3s </span><span class="token function" style="color:#e6db74">node</span><span class="token plain"> to be ready</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.wait_for:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        path: /usr/local/bin/k3s</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        state: present</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        timeout: </span><span class="token number" style="color:#ae81ff">300</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Ensure ~/.kube directory exists</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.file:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        path: /root/.kube</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        state: directory</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        mode: </span><span class="token string" style="color:#a6e22e">'0700'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Copy k3s kubeconfig</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.copy:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        remote_src: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        src: /etc/rancher/k3s/k3s.yaml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: /root/.kube/config</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        owner: root</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        group: root</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        mode: </span><span class="token string" style="color:#a6e22e">'0600'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Replace localhost with public IP </span><span class="token keyword" style="color:#66d9ef">in</span><span class="token plain"> kubeconfig</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.replace:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        path: /root/.kube/config</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        regexp: </span><span class="token string" style="color:#a6e22e">"server: https://127.0.0.1:6443"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        replace: </span><span class="token string" style="color:#a6e22e">"server: https://{{ inventory_hostname }}:6443"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Download Helm tarball</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.get_url:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        url: </span><span class="token string" style="color:#a6e22e">"https://get.helm.sh/helm-{{ helm_version }}-linux-amd64.tar.gz"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: </span><span class="token string" style="color:#a6e22e">"/tmp/helm.tar.gz"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        mode: </span><span class="token string" style="color:#a6e22e">'0644'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Extract Helm tarball</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.unarchive:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        src: </span><span class="token string" style="color:#a6e22e">"/tmp/helm.tar.gz"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: </span><span class="token string" style="color:#a6e22e">"/tmp/"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        remote_src: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Move Helm binary to /usr/local/bin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.command:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        cmd: </span><span class="token function" style="color:#e6db74">mv</span><span class="token plain"> /tmp/linux-amd64/helm /usr/local/bin/helm</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        creates: /usr/local/bin/helm</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Ensure python3-pip is installed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.apt:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        name: python3-pip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        state: present</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        update_cache: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Install Python kubernetes library</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.pip:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        name: kubernetes</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        executable: pip3</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Extract Helm binary</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.unarchive:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        src: /tmp/helm.tar.gz</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: /tmp</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        remote_src: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        extra_opts: </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">--strip-components</span><span class="token operator" style="color:#66d9ef">=</span><span class="token number" style="color:#ae81ff">1</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        creates: /tmp/helm</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Move Helm binary to /usr/local/bin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.command:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        cmd: </span><span class="token function" style="color:#e6db74">mv</span><span class="token plain"> /tmp/helm /usr/local/bin/helm</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        creates: /usr/local/bin/helm</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Copy Helm chart to server</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ansible.builtin.copy:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        src: </span><span class="token string" style="color:#a6e22e">"../../helm/helm_charts"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        dest: /home/ubuntu/</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> app_name </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        owner: ubuntu</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        group: ubuntu</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        mode: </span><span class="token string" style="color:#a6e22e">'0755'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        force: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Ensure namespace exists - </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> app_namespace </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      kubernetes.core.k8s:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        api_version: v1</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        kind: Namespace</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        name: </span><span class="token string" style="color:#a6e22e">"{{ app_namespace }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        kubeconfig: </span><span class="token string" style="color:#a6e22e">"{{ kubeconfig_path }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    - name: Deploy stack via Helm - </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> app_name </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      kubernetes.core.helm:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        name: </span><span class="token string" style="color:#a6e22e">"{{ app_namespace }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        chart_ref: /home/ubuntu/</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> app_name </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">/helm_charts</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        release_namespace: </span><span class="token string" style="color:#a6e22e">"{{ app_namespace }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        kubeconfig: </span><span class="token string" style="color:#a6e22e">"{{ kubeconfig_path }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create_namespace: </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        values_files:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          - /home/ubuntu/</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> app_name </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">/helm_charts/values.yaml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        values:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          ecrImageFull: </span><span class="token string" style="color:#a6e22e">"{{ aws_account_id }}.dkr.ecr.us-west-2.amazonaws.com/{{ app_name }}:{{ image_tag }}"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        force: </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        atomic: </span><span class="token boolean" style="color:#ae81ff">false</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-7---configure-aws-profile">Step 7 - Configure AWS Profile<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#step-7---configure-aws-profile" class="hash-link" aria-label="Direct link to Step 7 - Configure AWS Profile" title="Direct link to Step 7 - Configure AWS Profile" translate="no">​</a></h3>
<p>Open or create file <code>~/.aws/credentials</code> and add (if not already there):</p>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">[myaws]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_access_key_id = &lt;your_access_key&gt;</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_secret_access_key = &lt;your_secret_key&gt;</span><br></span></code></pre></div></div>
<p>Then use</p>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws configure</span><br></span></code></pre></div></div>
<p>And configure your AWS credentials</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-8---run-deployment">Step 8 - Run deployment<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#step-8---run-deployment" class="hash-link" aria-label="Direct link to Step 8 - Run deployment" title="Direct link to Step 8 - Run deployment" translate="no">​</a></h3>
<p>All deployment-related actions are automated, so no additional actions are required. To deploy the application, you only need to enter a few commands listed below and wait a few minutes. After that, you will be able to connect to the web application using the link you will receive in <code>terraform_output</code>. Next, if you wish, you can add GitHub Actions. To do this, follow the instructions in <a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#chellenges-when-you-build-on-ci" target="_blank" rel="noopener noreferrer" class="">our other post</a>.</p>
<p>In ../deploy/terraform folder</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init</span><br></span></code></pre></div></div>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="all-done">All done!<a href="https://adminforth.dev/blog/k3s-ec2-deployment/#all-done" class="hash-link" aria-label="Direct link to All done!" title="Direct link to All done!" translate="no">​</a></h3>
<p>Your application is now deployed on Amazon EC2 and available on the Internet.</p>]]></content>
        <author>
            <name>Kyrylo Doropii</name>
            <uri>https://github.com/kirilldorr</uri>
        </author>
        <category label="AWS" term="AWS"/>
        <category label="Terraform" term="Terraform"/>
        <category label="Helm" term="Helm"/>
        <category label="k3s" term="k3s"/>
        <category label="Ansible" term="Ansible"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[How to set up Context7 MCP in Visual Studio Code]]></title>
        <id>https://adminforth.dev/blog/context7-setup-vscode/</id>
        <link href="https://adminforth.dev/blog/context7-setup-vscode/"/>
        <updated>2025-10-21T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Context7 MCP installation guide]]></summary>
        <content type="html"><![CDATA[<p>This guide shows how you can set up Context7 MCP in your Visual Studio Code IDE (VS Code)</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="preparation">Preparation<a href="https://adminforth.dev/blog/context7-setup-vscode/#preparation" class="hash-link" aria-label="Direct link to Preparation" title="Direct link to Preparation" translate="no">​</a></h3>
<p>First of all, you'll need to update VS Code to version 1.105.1 or higher. To check your version, go to <code>Help -&gt; About</code>.</p>
<p>If you used a <code>.deb</code> file to install VS Code, just run these commands in the terminal to install updates:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">apt</span><span class="token plain"> update </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">apt</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">install</span><span class="token plain"> code</span><br></span></code></pre></div></div>
<p>Before opening VS Code, we need to get the Context7 API key. For this, go to <a href="https://context7.com/dashboard" target="_blank" rel="noopener noreferrer" class="">https://context7.com/dashboard</a>, create a new account, and copy the API key.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="setup">Setup<a href="https://adminforth.dev/blog/context7-setup-vscode/#setup" class="hash-link" aria-label="Direct link to Setup" title="Direct link to Setup" translate="no">​</a></h3>
<p>Now we can proceed with the setup:</p>
<ol>
<li class="">
<p>After opening VS Code, go to the extensions tab, where you'll see the new <code>MCP SERVERS</code> tab. Click on <code>Enable MCP Servers Marketplace</code>.
<img decoding="async" loading="lazy" src="https://adminforth.dev/assets/images/image_1-7c5253ddb0b1e52349fe6499bfdf757d.png" width="640" height="896" class="img_ev3q"></p>
</li>
<li class="">
<p>Find Context7 in the list and press <code>Install</code>:
<img decoding="async" loading="lazy" src="https://adminforth.dev/assets/images/image_2-9f03015fcc9342f4d3999bbd1a216931.png" width="640" height="896" class="img_ev3q"></p>
</li>
<li class="">
<p>Go to <code>Show Configuration JSON</code>:
<img decoding="async" loading="lazy" src="https://adminforth.dev/assets/images/image_3-d47a1fd2ba1e36d8e7980946d4100878.png" width="559" height="585" class="img_ev3q"></p>
</li>
<li class="">
<p>Click <code>Edit</code>:
<img decoding="async" loading="lazy" src="https://adminforth.dev/assets/images/image_4-8d306951c3bad63c41ecf5fa4a405a28.png" width="1216" height="507" class="img_ev3q"></p>
</li>
<li class="">
<p>Insert your API key:
<img decoding="async" loading="lazy" src="https://adminforth.dev/assets/images/image_5-b42f0e4b4377c3021799292b22f804c8.png" width="1079" height="545" class="img_ev3q"></p>
</li>
</ol>
<p>The installation is complete. You can now use it.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="example-installation-of-the-bulk-ai-flow-plugin">Example: Installation of the Bulk-ai-flow Plugin<a href="https://adminforth.dev/blog/context7-setup-vscode/#example-installation-of-the-bulk-ai-flow-plugin" class="hash-link" aria-label="Direct link to Example: Installation of the Bulk-ai-flow Plugin" title="Direct link to Example: Installation of the Bulk-ai-flow Plugin" translate="no">​</a></h3>
<p>Here is an example prompt you can use to add the adminforth bulk-ai-flow plugin:</p>
<p><strong>Prompt:</strong><br>
<code>Add adminforth bulk-ai-flow plugin to this file using Context7 MCP</code></p>
<p><strong>Generated Code:</strong><br>
<img decoding="async" loading="lazy" src="https://adminforth.dev/assets/images/image_6-5f8a8d74cdabfa0d8dfbdb8a91409899.png" width="1504" height="1114" class="img_ev3q"></p>
<p><strong>Result:</strong><br>
<img decoding="async" loading="lazy" src="https://adminforth.dev/assets/images/image_7-40ccac2a688dce138c72a7b0a8356cc8.png" width="2560" height="1351" class="img_ev3q"></p>
<p>As we can see, the generation works really well.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="tips">Tips<a href="https://adminforth.dev/blog/context7-setup-vscode/#tips" class="hash-link" aria-label="Direct link to Tips" title="Direct link to Tips" translate="no">​</a></h3>
<p>If you don’t want to add <code>use context7</code> to every prompt, you can <a href="https://github.com/upstash/context7?tab=readme-ov-file#-tips" target="_blank" rel="noopener noreferrer" class="">define a simple rule in your MCP client's rule section</a>.</p>
<p>If you're using github copilot, you can:</p>
<ol>
<li class="">In the root of your repository, create the .github directory if it does not already exist.</li>
<li class="">create a file named <code>.github/copilot-instructions.md</code></li>
<li class="">Inside of new file add:</li>
</ol>
<div class="language-txt codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-txt codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">Always use context7 when I need code generation, setup or configuration steps, or</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">library/API documentation. This means you should automatically use the Context7 MCP</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">tools to resolve library id and get library docs without me having to explicitly ask.</span><br></span></code></pre></div></div>]]></content>
        <author>
            <name>Yaroslav Pechorkin</name>
            <uri>https://github.com/yaroslav8765</uri>
        </author>
        <category label="Context7" term="Context7"/>
        <category label="MCP" term="MCP"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[How to translate dynamic strings in AdminForth API]]></title>
        <id>https://adminforth.dev/blog/dynamic-strings-translation/</id>
        <link href="https://adminforth.dev/blog/dynamic-strings-translation/"/>
        <updated>2025-04-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Simple example of how to translate dynamic strings from database in AdminForth API]]></summary>
        <content type="html"><![CDATA[<p>When you are using <a href="https://adminforth.dev/docs/tutorial/Plugins/i18n/#translating-external-application" target="_blank" rel="noopener noreferrer" class="">AdminForth i18n plugin for external Apps translation</a> you might face a case when you need to translate some data stored in your database which potentially can be changed in future.</p>
<p>Let's consider simple example where we have a Page resource</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> AdminForthResourceInput </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"adminforth"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">default</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  dataSource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"maindb"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  table</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"pages"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  resourceId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"pages"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"Pages"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  columns</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"url"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      primaryKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> all</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"meta_title"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"Meta Title"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"string"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> all</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"meta_desc"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"Meta Description"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"string"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> all</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">as</span><span class="token plain"> AdminForthResourceInput</span><span class="token punctuation" style="color:#f8f8f2">;</span><br></span></code></pre></div></div>
<p>You might have this page and return it in your API for nuxt:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">*</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">as</span><span class="token plain"> z </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"zod"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">app</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">get</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">admin</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">config</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">baseUrl</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/api/get_page</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">express</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">withSchema</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      description</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Returns translated SEO metadata for the page specified by the pageUrl query parameter.'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      response</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> z</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">object</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        meta_title</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> z</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        meta_desc</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> z</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">req</span><span class="token operator" style="color:#66d9ef">:</span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> res</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> Response</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">Promise</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token keyword" style="color:#66d9ef">void</span><span class="token operator" style="color:#66d9ef">&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> pageUrl </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> req</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">query</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">pageUrl</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token operator" style="color:#66d9ef">!</span><span class="token plain">pageUrl</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        res</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">status</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token number" style="color:#ae81ff">400</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">json</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> error</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"pageUrl is required"</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> page </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">"pages"</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">get</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">EQ</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">"url"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> pageUrl</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token operator" style="color:#66d9ef">!</span><span class="token plain">page</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        res</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">status</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token number" style="color:#ae81ff">404</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">json</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> error</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">Page not found </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">pageUrl</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      res</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">json</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        meta_title</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> page</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">meta_title</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        meta_desc</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> page</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">meta_desc</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><br></span></code></pre></div></div>
<p>Install and import Zod before using this pattern: <code>pnpm add zod</code> or <code>npm install zod</code>, then <code>import * as z from 'zod';</code>. <code>admin.express.withSchema(...)</code> will convert the Zod schema to OpenAPI for you.</p>
<p>Now you want to translate page meta title and meta description. You can do this by using <code>i18n</code> plugin for AdminForth.</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> AdminForth </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"adminforth"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">*</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">as</span><span class="token plain"> z </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"zod"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> </span><span class="token constant" style="color:#e6db74">SEO_PAGE_CATEGORY</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"seo_page_config"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">app</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">get</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">admin</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">config</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">baseUrl</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/api/get_page</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">\</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">express</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">withSchema</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Returns translated SEO metadata for the page specified by the pageUrl query parameter.'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    response</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> z</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">object</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      meta_title</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> z</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      meta_desc</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> z</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain"> admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">express</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">translatable</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">req</span><span class="token operator" style="color:#66d9ef">:</span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> res</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> Response</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">Promise</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token keyword" style="color:#66d9ef">void</span><span class="token operator" style="color:#66d9ef">&gt;</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> pageUrl </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> req</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">query</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">pageUrl</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token operator" style="color:#66d9ef">!</span><span class="token plain">pageUrl</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        res</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">status</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token number" style="color:#ae81ff">400</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">json</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> error</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"pageUrl is required"</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> page </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">"pages"</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">get</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">EQ</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">"url"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> pageUrl</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token operator" style="color:#66d9ef">!</span><span class="token plain">page</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        res</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">status</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token number" style="color:#ae81ff">404</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">json</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> error</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">Page not found </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">pageUrl</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> translateKeys </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"meta_title"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"meta_desc"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">meta_title</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> meta_desc</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">Promise</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">all</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">translateKeys</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">map</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">key</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> req</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">tr</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">page</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">key</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token constant" style="color:#e6db74">SEO_PAGE_CATEGORY</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      res</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">json</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-remove-line" style="color:#f8f8f2"><span class="token plain">        meta_title</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> page</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">meta_title</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        meta_title</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-remove-line" style="color:#f8f8f2"><span class="token plain">        meta_desc</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> page</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">meta_desc</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        meta_desc</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><br></span></code></pre></div></div>
<p>Looks straightforward, but here are 2 issues:</p>
<p>Since translation strings are created only when first time you call <code>req.tr</code> function, you need to ensure you will call this API for all your pages (in all envs e.g. local, dev, staging, prod ), this might be not convinient - you have to call this API, then go to translation page and translate it with bulk action LLM or manually, but you might forget one page or so.</p>
<p>To fix this we suggest next, go to Page resource and add next function on top level:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line code-block-diff-remove-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> AdminForthResourceInput </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"adminforth"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> AdminForth</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> AdminForthResourceInput</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> Filters</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> IAdminForth </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"adminforth"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> I18nPlugin </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"@adminforth/i18n/index.js"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> </span><span class="token constant" style="color:#e6db74">SEO_PAGE_CATEGORY</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"../api.ts"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">function</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">feedAllPageTranslations</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">adminforth</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> IAdminForth</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> i18nPlugin </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> adminforth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token generic-function function" style="color:#e6db74">getPluginByClassName</span><span class="token generic-function generic class-name operator" style="color:#66d9ef">&lt;</span><span class="token generic-function generic class-name" style="color:#e6db74">I18nPlugin</span><span class="token generic-function generic class-name operator" style="color:#66d9ef">&gt;</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'I18nPlugin'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> pages </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> adminforth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'pages'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">list</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">Promise</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">all</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    pages</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">map</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">page</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">Promise</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">all</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token string" style="color:#a6e22e">'meta_title'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'meta_desc'</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">map</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">key</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">page</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">key</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> i18nPlugin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">feedCategoryTranslations</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> en_string</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> page</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">key</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> source</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">pages.</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">page</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">url</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">.</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">key</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token constant" style="color:#e6db74">SEO_PAGE_CATEGORY</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">default</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  dataSource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"maindb"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  table</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"pages"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  resourceId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"pages"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token operator" style="color:#66d9ef">...</span><br></span></code></pre></div></div>
<p>This function will iterate all pages and call <code>feedCategoryTranslations</code> function for each page and each key. This will create translation strings in your database for each page and each key. If translation objects already exist, it will skip them (<strong>will not</strong> create duplicates and <strong>will not</strong> overwrite translations).</p>
<p>Now we need to call this function in hooks:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">default</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  dataSource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"maindb"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  table</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"pages"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  resourceId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"pages"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"Pages"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  hooks</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">afterSave</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> record</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminforth </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> record</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> Record</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token operator" style="color:#66d9ef">&gt;</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminforth</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> IAdminForth </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token function" style="color:#e6db74">feedAllPageTranslations</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">adminforth</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> ok</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">afterSave</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> oldRecord</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> updates</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminforth </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> oldRecord</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> Record</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token operator" style="color:#66d9ef">&gt;</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> updates</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> Record</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token operator" style="color:#66d9ef">&gt;</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminforth</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> IAdminForth </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token function" style="color:#e6db74">feedAllPageTranslations</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">adminforth</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> ok</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"> </span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">  </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  columns</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token operator" style="color:#66d9ef">...</span><br></span></code></pre></div></div>
<blockquote>
<p>Please note that we run this function without await, so it will not block your API. SO function will be called in background when hook will already return and user will not wait for it.</p>
</blockquote>
<p>Now every time you will create or edit page, it will call <code>feedAllPageTranslations</code> function and create translation strings for each page and each key.
You can also import this function into index script and run after database discover, if you already have pages in your database and you want to create translation strings for them even without clicking on create or edit button.</p>
<h1>Issue 2 - after modification of page attributes old translation strings will not be removed</h1>
<p>You can mitigate this by adding couple of lines into edit hook:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">    edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">afterSave</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> oldRecord</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> updates</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminforth </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> oldRecord</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> Record</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token operator" style="color:#66d9ef">&gt;</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> updates</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> Record</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token operator" style="color:#66d9ef">&gt;</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminforth</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> IAdminForth </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">         </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">Object</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">keys</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">updates</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">length</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token comment" style="color:#8292a2;font-style:italic">// find old strings which were edited and which are not used anymore</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> oldStrings </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> adminforth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'translations'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">list</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">            Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AND</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">              Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">EQ</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'category'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'seo_page_config'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">              Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">IN</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'en_string'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> Object</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">keys</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">updates</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">map</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">key</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> oldRecord</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">key</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token comment" style="color:#8292a2;font-style:italic">// delete them</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">Promise</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">all</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">            oldStrings</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">map</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">oldString</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">              </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> adminforth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'translations'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">delete</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">oldString</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token function" style="color:#e6db74">feedAllPageTranslations</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">adminforth</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> ok</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><br></span></code></pre></div></div>
<p>This will delete all old translation strings which are not used anymore.</p>
<p>If your have ability to delete pages, you can also add delete hook, you can do this as a homework.</p>
<h1>Conclusion</h1>
<p>In this article we have shown how to translate dynamic strings in AdminForth API. We have also shown how to create translation strings for each page and each key in your database.
We have also shown how to delete old translation strings which are not used anymore. This will help you to keep your translation strings clean and up to date.</p>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="Keycloak" term="Keycloak"/>
        <category label="Auth" term="Auth"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Setup AdminForth Authorization via Keycloak]]></title>
        <id>https://adminforth.dev/blog/keycloak-setup-example/</id>
        <link href="https://adminforth.dev/blog/keycloak-setup-example/"/>
        <updated>2025-03-25T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The ultimate guide to setting up AdminForth authorization via Keycloak]]></summary>
        <content type="html"><![CDATA[<p>Keycloak is an open-source identity and access management solution that provides authentication and authorization services. It can be used to secure applications and services by managing user identities, roles, and permissions.</p>
<p>In this guide, we will walk you through the process of setting up AdminForth authorization via Keycloak. Most important we will show you how to set up Keycloak in a Docker container and configure it to work with AdminForth.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="prerequisites">Prerequisites<a href="https://adminforth.dev/blog/keycloak-setup-example/#prerequisites" class="hash-link" aria-label="Direct link to Prerequisites" title="Direct link to Prerequisites" translate="no">​</a></h2>
<ul>
<li class="">Docker installed on your machine</li>
<li class="">Basic knowledge of Docker and Docker Compose</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-create-a-docker-compose-file">Step 1: Create a Docker Compose File<a href="https://adminforth.dev/blog/keycloak-setup-example/#step-1-create-a-docker-compose-file" class="hash-link" aria-label="Direct link to Step 1: Create a Docker Compose File" title="Direct link to Step 1: Create a Docker Compose File" translate="no">​</a></h2>
<p>Create a <code>docker-compose.yml</code> file in your project directory. This file will define the Keycloak service and its configuration.</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">services</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">pg</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> postgres</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">environment</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">POSTGRES_USER</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> demo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">POSTGRES_PASSWORD</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> demo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">POSTGRES_DB</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> demo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"5432:5432"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> pg</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">data</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">/var/lib/postgresql/data</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">keycloak</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> quay.io/keycloak/keycloak</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> start</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">dev</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">environment</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> KEYCLOAK_ADMIN=admin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> KEYCLOAK_ADMIN_PASSWORD=admin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DB_VENDOR=postgres</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DB_ADDR=pg</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DB_DATABASE=demo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DB_USER=demo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DB_PASSWORD=demo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"8080:8080"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">depends_on</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> pg</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> keycloak</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">data</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">/opt/keycloak/data</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  keycloak</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">data</span><span class="token punctuation" style="color:#f8f8f2">:</span><br></span></code></pre></div></div>
<p>Run service:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">docker</span><span class="token plain"> compose </span><span class="token parameter variable" style="color:#f8f8f2">-p</span><span class="token plain"> af-dev-demo up </span><span class="token parameter variable" style="color:#f8f8f2">-d</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">--build</span><span class="token plain"> --remove-orphans </span><span class="token parameter variable" style="color:#f8f8f2">--wait</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-singn-in-to-keycloak-and-create-a-keycloak-realm">Step 2: Singn in to Keycloak and Create a Keycloak Realm<a href="https://adminforth.dev/blog/keycloak-setup-example/#step-2-singn-in-to-keycloak-and-create-a-keycloak-realm" class="hash-link" aria-label="Direct link to Step 2: Singn in to Keycloak and Create a Keycloak Realm" title="Direct link to Step 2: Singn in to Keycloak and Create a Keycloak Realm" translate="no">​</a></h2>
<ol>
<li class="">Open the Keycloak UI at <code>http://localhost:8080/</code>.</li>
<li class="">Sign in with the credentials <code>admin</code> and <code>admin</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-3-aae1be024f2b0465f343a3afd78e8705.png" width="1110" height="685" class="img_ev3q"></p>
<ol start="3">
<li class="">Select the "Realms" tab and click <code>Create Realm</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-4-2261eee7e0672de25377bad6cbb4264f.png" width="1342" height="871" class="img_ev3q"></p>
<ol start="4">
<li class="">Enter a name for your realm and click <code>Create</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-5-434b5be3b4467a6d3bb8ea980d8a49ee.png" width="1104" height="614" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-create-a-keycloak-client">Step 3: Create a Keycloak Client<a href="https://adminforth.dev/blog/keycloak-setup-example/#step-3-create-a-keycloak-client" class="hash-link" aria-label="Direct link to Step 3: Create a Keycloak Client" title="Direct link to Step 3: Create a Keycloak Client" translate="no">​</a></h2>
<ol>
<li class="">Go to <code>Clients</code> tab and click <code>Create Client</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-10-10a08b7e071d3604d5164d097c20fa26.png" width="1859" height="848" class="img_ev3q"></p>
<ol start="2">
<li class="">Choose <code>OpenID Connect</code>, enter a client ID for your client and click <code>Next</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-72c12c886ef5f1fdf13e3f69a9d786ff.png" width="1399" height="630" class="img_ev3q"></p>
<ol start="3">
<li class="">Swith <code>Client authentication</code> to <code>On</code> and click <code>Next</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-1-ce94235b0debb8fb2480393295b88ff9.png" width="1399" height="630" class="img_ev3q"></p>
<ol start="4">
<li class="">Enter a <code>Valid redirect URI</code> and click <code>Save</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-9-9e90fad3645208faf3d7a89580ba2124.png" width="1393" height="481" class="img_ev3q"></p>
<ol start="5">
<li class="">In the <code>Client details</code> go to <code>Credentials</code> tab and copy the <code>Client secret</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-2-15bcedc74c03e9e768fb34903fb5a97f.png" width="1110" height="685" class="img_ev3q"></p>
<ol start="6">
<li class="">Add the credentials to your <code>.env</code> file:</li>
</ol>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token assign-left variable" style="color:#f8f8f2">KEYCLOAK_CLIENT_ID</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">your_keycloak_client_id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token assign-left variable" style="color:#f8f8f2">KEYCLOAK_CLIENT_SECRET</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">your_keycloak_client_secret</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token assign-left variable" style="color:#f8f8f2">KEYCLOAK_REALM</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">your_keycloak_realm</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token assign-left variable" style="color:#f8f8f2">KEYCLOAK_URL</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">http://localhost:8080</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4-create-a-keycloak-user">Step 4: Create a Keycloak User<a href="https://adminforth.dev/blog/keycloak-setup-example/#step-4-create-a-keycloak-user" class="hash-link" aria-label="Direct link to Step 4: Create a Keycloak User" title="Direct link to Step 4: Create a Keycloak User" translate="no">​</a></h2>
<ol>
<li class="">Go to <code>Users</code> tab and click <code>Create new user</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-6-2378b1f24c43984cf8fe3b9df6531caa.png" width="2267" height="841" class="img_ev3q"></p>
<ol start="2">
<li class="">Enter a <code>Username</code>, <code>Email</code>, <code>First name</code> and <code>Last name</code> and click <code>Create</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-7-28621265de2379ceb2d7764a9e8caafc.png" width="1081" height="756" class="img_ev3q"></p>
<ol start="3">
<li class="">In the  <code>User details</code> go to <code>Credentials</code> tab and click <code>Set password</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-12-2065ef60cc056a3800ceb2c930ee3df0.png" width="1859" height="848" class="img_ev3q"></p>
<ol start="4">
<li class="">Enter a <code>Password</code>, turn <code>Temporary</code> to <code>Off</code> and click <code>Save</code>.</li>
</ol>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-8-7d2e6af907b827e7f1d362b565d6290f.png" width="592" height="345" class="img_ev3q"></p>
<p>Finally, you can sign in to AdminForth with your Keycloak credentials. (if user with the same email exists in AdminForth)</p>]]></content>
        <author>
            <name>Maksym Pipkun</name>
            <uri>https://github.com/NoOne7135</uri>
        </author>
        <category label="Keycloak" term="Keycloak"/>
        <category label="Auth" term="Auth"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[IaaC Simplified: Amazon EC2 Deployments with GitHub Actions, Terraform, Docker & Amazon ECR]]></title>
        <id>https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/</id>
        <link href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/"/>
        <updated>2025-02-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The ultimate step-by-step guide to cost-effective, build-time-efficient, and easy managable EC2 deployments using GitHub Actions, Terraform, Docker, and a Amazon ECR registry.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/ga-tf-ecr-87ced7681bcc3685f507cc9252bc0ffe.jpg" width="1200" height="630" class="img_ev3q"></p>
<p>This guide shows how to deploy own Docker apps (with AdminForth as example) to Amazon EC2 instance with Docker and Terraform involving pushing images into Amazon ECR.</p>
<p>Needed resources:</p>
<ul>
<li class="">GitHub actions Free plan which includes 2000 minutes per month (1000 of 2-minute builds per month - more then enough for many projects, if you are not running tests). Extra builds would cost <code>0.008$</code> per minute.</li>
<li class="">AWS account where we will auto-spawn EC2 instance. We will use <code>t3a.small</code> instance (2 vCPUs, 2GB RAM) which costs <code>~14$</code> per month in <code>us-east-1</code> region (cheapest region). Also it will take <code>$2</code> per month for EBS gp2 storage (20GB) for EC2 instance.</li>
<li class="">Also AWS ECR will charge for <code>$0.09</code> per GB of data egress traffic (from EC2 to the internet) - this needed to load docker build cache.</li>
</ul>
<p>The setup shape:</p>
<ul>
<li class="">Build is done using IaaC approach with HashiCorp Terraform, so almoast no manual actions are needed from you. Every resource including EC2 server instance is described in code which is commited to repo.</li>
<li class="">Docker build process is done on GitHub actions server, so EC2 server is not overloaded with builds</li>
<li class="">Changes in infrastructure including changing server type, adding S3 Bucket, changing size of sever disk is also can be done by commiting code to repo.</li>
<li class="">Docker images and build cache are stored on Amazon ECR</li>
<li class="">Total build time for average commit to AdminForth app (with Vite rebuilds) is around 2 minutes.</li>
</ul>
<p>Previously we had a blog post about <a class="" href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/">deploying AdminForth to EC2 with Terraform without registry</a>. That method might work well but has a significant disadvantage - build process happens on EC2 itself and uses EC2 RAM and CPU. This can be a problem if your EC2 instance is well-loaded without extra free resources. Moreover, low-end EC2 instances have a small amount of RAM and CPU, so build process which involves vite/tsc/etc can be slow or even fail / cause OOM killer to crash EC2 instance.</p>
<p>So obviously to solve this problem we need to move the build process to CI, however it introduces new chellenges and we will solve them in this post.</p>
<p>Quick difference between approaches from previous post and current post:</p>
<table><thead><tr><th>Feature</th><th>Without Registry</th><th>With ECR Registry</th></tr></thead><tbody><tr><td>How build happens</td><td>Source code is rsync-ed from CI to EC2 and docker build is done there</td><td>Docker build is done on CI and docker image is pushed to registry, then Docker on EC2 pulls from registry</td></tr><tr><td>Where build is done</td><td>On EC2</td><td>On CI</td></tr><tr><td>How Docker build layers are cached</td><td>Cache is stored on EC2</td><td>GitHub actions has no own Docker cache out of the box, so it should be stored in dedicated place (we use Amazon ECR)</td></tr><tr><td>Advantages</td><td>Cheaper (no egrass cache traffik from EC2) and faster</td><td>Build is done on CI, so EC2 server is not overloaded</td></tr><tr><td>Disadvantages</td><td>Build on EC2 requires additional server RAM / requires swap / overloads CPU</td><td>More terraform code is needed. Extra cost for egress traffik to GitHub for cache transfer</td></tr><tr><td>Initial build time*</td><td>3m 13.541s</td><td>3m 54s</td></tr><tr><td>Rebuild time (changed <code>index.ts</code>)*</td><td>0m 51.653s</td><td>0m 54.120s</td></tr></tbody></table>
<sub>* All tests done from local machine (Intel(R) Core(TM) Ultra 9 185H, Docker Desktop/WSL2 64 GB RAM, 300Mbps up/down) up to working state</sub>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="chellenges-when-you-build-on-ci">Chellenges when you build on CI<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#chellenges-when-you-build-on-ci" class="hash-link" aria-label="Direct link to Chellenges when you build on CI" title="Direct link to Chellenges when you build on CI" translate="no">​</a></h2>
<p>A little bit of theory.</p>
<p>When you move build process to CI you have to solve next chellenges:</p>
<ol>
<li class="">We need to deliver built docker images to EC2 somehow (and only we)</li>
<li class="">We need to persist cache between builds</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="delivering-images">Delivering images<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#delivering-images" class="hash-link" aria-label="Direct link to Delivering images" title="Direct link to Delivering images" translate="no">​</a></h3>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="exporing-images-to-tar-files">Exporing images to tar files<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#exporing-images-to-tar-files" class="hash-link" aria-label="Direct link to Exporing images to tar files" title="Direct link to Exporing images to tar files" translate="no">​</a></h4>
<p>Simplest option which you can find is save docker images to tar files and deliver them to EC2. We can easily do it in terraform (using <code>docker save -o ...</code> command on CI and <code>docker load ...</code> command on EC2). However this option has a significant disadvantage - it is slow. Docker images are big (always include all layers, without any options), so it takes infinity to do save/load and another infinity to transfer them to EC2 (via relatively slow rsync/SSH and relatively slow GitHub actions outbound connection).</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="docker-registry">Docker registry<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#docker-registry" class="hash-link" aria-label="Direct link to Docker registry" title="Direct link to Docker registry" translate="no">​</a></h4>
<p>Faster, right option which we will use here - involve Docker registry. Registry is a repository which stores docker images. It does it in a smart way - it saves each image as several layers, so if you will update last layer, then only last layer will be pushed to registry and then only last will be pulled to EC2.
To give you row compare - whole-layers image might take <code>1GB</code>, but last layer created by <code>npm run build</code> command might take <code>50MB</code>. And most builds you will do only last layer changes, so it will be 20 times faster to push/pull last layer than whole image.
And this is not all, registry uses TLS HTTP protocol so it is faster then SSH/rsync encrypted connection.</p>
<p>Of course you have to care about a way of registry authentication (so only you and your CI/EC2 can push/pull images).</p>
<p>What docker registry can you use? Pretty known options:</p>
<ol>
<li class="">Docker Hub - most famous. It is free for public images, so literally every opensource project uses it. However it is not free for private images, and you have to pay for it. Payment model is pretty strange - you pay for user who can login, like 11$ per month, you might pay for your devops only but all this sounds strange.</li>
<li class="">GHCR - Registry from GitHub. Has free plan but allows to store only 500MB and allows to transfer 1GB of traffic per month. Then you pay for every extra GB in storage (<code>$0.0008</code> per GB/day or <code>$0.24</code> per GB/month) and for every extra GB in traffic ($0.09 per GB). Probably small images will fit in free plan, but generally even alpine-based docker images are bigger than 500MB, so it is non-free option.</li>
<li class="">Amazon ECR - Same as GHCR but from Amazon. Price is <code>$0.10</code> per GB of storage per month and <code>$0.09</code> per GB of data transfer from Amazon (as all Amazon egress traffic). So it is cheaper than GHCR.</li>
<li class="">Self-hosted registry web system. In our software development company, we use Harbor. It is a powerful free open-source registry that can be installed to own server. It allows pushing and pulling without limit. Also, it has internal life-cycle rules that cleanup unnecessary images and layers. The main drawbacks of it are that it is not so fast to install and configure, plus you have to get a domain and another powerfull server to run it. So unless you are a software development company, it is not worth using it.</li>
<li class="">Self-hosted minimal CNCF Distribution <a href="https://distribution.github.io/distribution/" target="_blank" rel="noopener noreferrer" class="">registry</a> on EC2 itself. So since we already have EC2, we can run registry on it directly. The <code>registry</code> container is pretty light-weight and it will not consume a lot of extra CPU/RAM on server. Plus images will be stored close to application so pull will be fast, however securing this right is a bit tricky. If you want to try it we have special <a class="" href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/">EC2 with CNCF registry post</a>.</li>
</ol>
<p>In the post we will use Amazon ECR as registry (3rd way).</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="persisting-cache">Persisting cache<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#persisting-cache" class="hash-link" aria-label="Direct link to Persisting cache" title="Direct link to Persisting cache" translate="no">​</a></h3>
<p>Docker builds without layer cache persistence are possible but very slow. Most builds only change a couple of layers, and having no ability to cache them will cause the Docker builder to regenerate all layers from scratch. This can, for example, increase the Docker build time from a minute to ten minutes or even more.</p>
<p>Out of the box, GitHub Actions can't save Docker layers between builds, so you have to use external storage.</p>
<blockquote>
<p>Though some CI systems can persist docker build cache, e.g. open-source self-hosted Woodpecker CI allows it out of the box. However GitHub actions which is pretty popular, reasonably can't allow such free storage to anyone</p>
</blockquote>
<p>So when build-in Docker cache can't be used, there is one alternative - Docker BuildKit external cache.
So BuildKit allows you to connect external storage. There are several options, but most sweet for us is using Docker registry as cache storage (not only as images storage to deliver them to application server).</p>
<p>Drawback is that buildx which is running on GitHub action server will download cache from registry which is inside of AWS. And all AWS egress traffic is charged. So you will pay for every build which uses cache. However cache is comnpressed. To give you idea basic alpine image with AdminForth cache is 180MB. One one commercial project we did full release within 2 months and 400 builds so it took <code>400 * 180MB * $0.09 / GB = $6.48</code> for cache transfer for whole project.</p>
<blockquote>
<p><em>BuildKit cache in Compose issue</em>
Previously we used docker compose to build &amp; run our app, it can be used to both build, push and pull images, but has <a href="https://github.com/docker/compose/issues/11072#issuecomment-1848974315" target="_blank" rel="noopener noreferrer" class="">issues with external cache connection</a>. While they are not solved we have to use <code>docker buildx bake</code> command to build images. It is not so bad, but is another point of configuration which we will cover in this post.</p>
</blockquote>
<h1>Prerequisites</h1>
<p>I will assume you run Ubuntu (Native or WSL2).</p>
<p>You should have terraform, here is official repository:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">sudo apt update &amp;&amp; sudo apt install terraform</span><br></span></code></pre></div></div>
<p>AWS CLI:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> snap </span><span class="token function" style="color:#e6db74">install</span><span class="token plain"> aws-cli </span><span class="token parameter variable" style="color:#f8f8f2">--classic</span><br></span></code></pre></div></div>
<p>Also you need Doker Daemon running. We recommend Docker Desktop running. ON WSL2 make sure you have Docker Desktop WSL2 integration enabled.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">docker</span><span class="token plain"> version</span><br></span></code></pre></div></div>
<h1>Practice - deploy setup</h1>
<p>Assume you have your AdminForth project in <code>myadmin</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1---dockerfile-and-dockerignore">Step 1 - Dockerfile and .dockerignore<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-1---dockerfile-and-dockerignore" class="hash-link" aria-label="Direct link to Step 1 - Dockerfile and .dockerignore" title="Direct link to Step 1 - Dockerfile and .dockerignore" translate="no">​</a></h2>
<p>This guide assumes you have created your AdminForth application with latest version of <code>adminforth create-app</code> command.
This command already creates a <code>Dockerfile</code> and <code>.dockerignore</code> for you, so you can use them as is.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2---composeyml">Step 2 - compose.yml<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-2---composeyml" class="hash-link" aria-label="Direct link to Step 2 - compose.yml" title="Direct link to Step 2 - compose.yml" translate="no">​</a></h2>
<p>create folder <code>deploy</code> and create file <code>compose.yml</code> inside:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/compose.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">services</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">traefik</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik:v2.5"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--api.insecure=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--providers.docker=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--entrypoints.web.address=:80"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"80:80"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"/var/run/docker.sock:/var/run/docker.sock:ro"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">myadmin</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">MYADMIN_REPO</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">build</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">context</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> ../adminforth</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">app</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">tags</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">MYADMIN_REPO</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">cache_from</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> type=registry</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">ref=$</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">MYADMIN_REPO</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">cache</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">cache_to</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> type=registry</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">ref=$</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">MYADMIN_REPO</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">cache</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">mode=max</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">compression=zstd</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">image</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">manifest=true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">oci</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">mediatypes=true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">pull_policy</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> always</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">restart</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> always</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">env_file</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> .env.secrets.prod</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> myadmin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">db</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">/code/db</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">labels</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.enable=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.routers.myadmin.rule=PathPrefix(`/`)"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.services.myadmin.loadbalancer.server.port=3500"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.routers.myadmin.priority=2"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  myadmin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">db</span><span class="token punctuation" style="color:#f8f8f2">:</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3---create-a-ssh-keypair">Step 3 - create a SSH keypair<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-3---create-a-ssh-keypair" class="hash-link" aria-label="Direct link to Step 3 - create a SSH keypair" title="Direct link to Step 3 - create a SSH keypair" translate="no">​</a></h2>
<p>Make sure you are still in <code>deploy</code> folder, run next command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">mkdir</span><span class="token plain"> .keys </span><span class="token operator" style="color:#66d9ef">&amp;&amp;</span><span class="token plain"> ssh-keygen </span><span class="token parameter variable" style="color:#f8f8f2">-f</span><span class="token plain"> .keys/id_rsa </span><span class="token parameter variable" style="color:#f8f8f2">-N</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">""</span><br></span></code></pre></div></div>
<p>Now it should create <code>deploy/.keys/id_rsa</code> and <code>deploy/.keys/id_rsa.pub</code> files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4---create-tls-certificates-to-encrypt-traffic-between-ci-and-registry">Step 4 - create TLS certificates to encrypt traffic between CI and registry<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-4---create-tls-certificates-to-encrypt-traffic-between-ci-and-registry" class="hash-link" aria-label="Direct link to Step 4 - create TLS certificates to encrypt traffic between CI and registry" title="Direct link to Step 4 - create TLS certificates to encrypt traffic between CI and registry" translate="no">​</a></h2>
<p>Make sure you are still in <code>deploy</code> folder, run next command:</p>
<p>Run next command to create TLS certificates:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">openssl req </span><span class="token parameter variable" style="color:#f8f8f2">-new</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-x509</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-days</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">3650</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-newkey</span><span class="token plain"> rsa:4096 </span><span class="token parameter variable" style="color:#f8f8f2">-nodes</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-keyout</span><span class="token plain"> .keys/ca.key </span><span class="token parameter variable" style="color:#f8f8f2">-subj</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"/CN=My Custom CA"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-out</span><span class="token plain"> .keys/ca.pem</span><br></span></code></pre></div></div>
<p>This will create <code>deploy/.keys/ca.key</code> and <code>deploy/.keys/ca.pem</code> files.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5---gitignore-file">Step 5 - .gitignore file<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-5---gitignore-file" class="hash-link" aria-label="Direct link to Step 5 - .gitignore file" title="Direct link to Step 5 - .gitignore file" translate="no">​</a></h2>
<p>Create <code>deploy/.gitignore</code> file with next content:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">.terraform/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.keys/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfstate</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfstate.*</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfvars</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">tfplan</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.env.secrets.prod</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6---file-with-secrets-for-local-deploy">Step 6 - file with secrets for local deploy<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-6---file-with-secrets-for-local-deploy" class="hash-link" aria-label="Direct link to Step 6 - file with secrets for local deploy" title="Direct link to Step 6 - file with secrets for local deploy" translate="no">​</a></h2>
<p>Create file <code>deploy/.env.secrets.prod</code></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token assign-left variable" style="color:#f8f8f2">ADMINFORTH_SECRET</span><span class="token operator" style="color:#66d9ef">=</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token plain">your_secret</span><span class="token operator" style="color:#66d9ef">&gt;</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-7---main-terraform-file-maintf">Step 7 - main terraform file main.tf<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-7---main-terraform-file-maintf" class="hash-link" aria-label="Direct link to Step 7 - main terraform file main.tf" title="Direct link to Step 7 - main terraform file main.tf" translate="no">​</a></h2>
<p>First of all install Terraform as described here <a href="https://developer.hashicorp.com/terraform/install#linux" target="_blank" rel="noopener noreferrer" class="">terraform installation</a>.</p>
<p>Create file <code>main.tf</code> in <code>deploy</code> folder:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">locals {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app_name = "testtf"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  aws_region = "us-east-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">provider "aws" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  region = local.aws_region</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  profile = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_ami" "ubuntu_linux" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  most_recent = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  owners      = ["amazon"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "name"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_vpc" "default" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  default = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_eip" "eip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> domain = "vpc"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_eip_association" "eip_assoc" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> instance_id   = aws_instance.app_instance.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> allocation_id = aws_eip.eip.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_subnet" "default_subnet" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "vpc-id"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = [data.aws_vpc.default.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "default-for-az"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["true"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "availability-zone"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["${local.aws_region}a"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_security_group" "instance_sg" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name   = "${local.app_name}-instance-sg"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id = data.aws_vpc.default.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow HTTP"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # SSH</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow SSH"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  egress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow all outbound traffic"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_key_pair" "app_deployer" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name   = "terraform-deploy_${local.app_name}-key"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_instance" "app_instance" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ami                    = data.aws_ami.ubuntu_linux.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  instance_type          = "t3a.small"  # just change it to another type if you need, check https://instances.vantage.sh/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_id              = data.aws_subnet.default_subnet.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_security_group_ids = [aws_security_group.instance_sg.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name               = aws_key_pair.app_deployer.key_name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  iam_instance_profile = aws_iam_instance_profile.ec2_profile.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # prevent accidental termination of ec2 instance and data loss</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # if you will need to recreate the instance still (not sure why it can be?), you will need to remove this block manually by next command:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # &gt; terraform taint aws_instance.app_instance</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  lifecycle {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    prevent_destroy = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ignore_changes = [ami]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  root_block_device {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_size = 20 // Size in GB for root partition</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_type = "gp2"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Even if the instance is terminated, the volume will not be deleted, delete it manually if needed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    delete_on_termination = false</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  user_data = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    #!/bin/bash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    set -euo pipefail</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    LOG_FILE="/home/ubuntu/user_data.log"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    exec &gt; &gt;(tee -a "$LOG_FILE") 2&gt;&amp;1</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    chown ubuntu:ubuntu "$LOG_FILE" || true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    touch /home/ubuntu/user_data_started</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    on_error() {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "failed" &gt; /home/ubuntu/user_data_failed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    trap on_error ERR</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    export DEBIAN_FRONTEND=noninteractive</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    apt-get update</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    apt-get install -y --no-install-recommends \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ca-certificates \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      curl</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Install the latest Docker Engine from Docker's official Ubuntu repo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    install -m 0755 -d /etc/apt/keyrings</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    chmod a+r /etc/apt/keyrings/docker.asc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      $(. /etc/os-release &amp;&amp; echo "$VERSION_CODENAME") stable" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      &gt; /etc/apt/sources.list.d/docker.list</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    apt-get update</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    apt-get install -y --no-install-recommends \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker-ce \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker-ce-cli \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      containerd.io \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker-buildx-plugin \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker-compose-plugin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Ensure Docker daemon is up</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl enable --now docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Allow ubuntu user to run docker without sudo (new SSH session required)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    usermod -aG docker ubuntu || true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    docker --version</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    docker compose version</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo "done" &gt; /home/ubuntu/user_data_done</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Name = "${local.app_name}-instance"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "wait_for_user_data" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'Waiting for EC2 software install to finish...'",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "while [ ! -f /home/ubuntu/user_data_done ]; do echo '...'; sleep 2; done",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'EC2 software install finished.'"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("./.keys/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_instance.app_instance]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_ecr_repository" "myadmin_repo" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name = "${local.app_name}-myadmin"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  force_delete = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_ecr_lifecycle_policy" "safe_cleanup" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  repository = aws_ecr_repository.myadmin_repo.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  policy = jsonencode({</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    rules = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        rulePriority = 1</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        description  = "Delete untagged images older than 7 days"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        selection = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          tagStatus     = "untagged"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          countType     = "sinceImagePushed"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          countUnit     = "days"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          countNumber   = 7</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        action = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          type = "expire"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  })</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "local_file" "compose_env" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  content  = "MYADMIN_REPO=${aws_ecr_repository.myadmin_repo.repository_url}"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filename = "${path.module}/.env.ecr"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">// allow ec2 instance to login to ECR too pull images</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_role" "ec2_role" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name = "${local.app_name}-ec2-role"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  assume_role_policy = jsonencode({</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Version = "2012-10-17",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Statement = [{</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      Effect = "Allow",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      Principal = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        Service = "ec2.amazonaws.com"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      },</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      Action = "sts:AssumeRole"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  })</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_role_policy_attachment" "ecr_access" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  role       = aws_iam_role.ec2_role.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_instance_profile" "ec2_profile" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name = "${local.app_name}-instance-profile"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  role = aws_iam_role.ec2_role.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "sync_files_and_run" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      aws ecr get-login-password --region ${local.aws_region} --profile myaws | docker login --username AWS --password-stdin ${aws_ecr_repository.myadmin_repo.repository_url}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Running build"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      env $(cat .env.ecr | grep -v "#" | xargs) docker buildx bake --progress=plain --push --allow=fs.read=.. -f compose.yml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # if you will change host, pleasee add -o StrictHostKeyChecking=no</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Copy files to the instance" </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      rsync -t -avz --mkpath -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --delete \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --exclude '.terraform' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --exclude '.keys' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --exclude 'tfplan' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        . ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/deploy/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Run docker compose after files have been copied</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [&lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      aws ecr get-login-password --region ${local.aws_region} | docker login --username AWS --password-stdin ${aws_ecr_repository.myadmin_repo.repository_url}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      cd /home/ubuntu/app/deploy</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Spinning up the app"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker compose --progress=plain -p app  --env-file .env.ecr -f compose.yml up -d --remove-orphans</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # cleanup unused cache (run in background to not block terraform)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      screen -dm docker system prune -f</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("./.keys/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Ensure the resource is triggered every time based on timestamp or file hash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    always_run = timestamp()</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_eip_association.eip_assoc, null_resource.wait_for_user_data]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">output "instance_public_ip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  value = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">######### META, tf state ##############</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"># S3 bucket for storing Terraform state</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = "${local.app_name}-terraform-state"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  rule {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    status = "Enabled"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    id = "Keep only the latest version of the state file"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      prefix = ""</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    noncurrent_version_expiration {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      noncurrent_days = 30</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_versioning" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  versioning_configuration {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    status = "Enabled"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  rule {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    apply_server_side_encryption_by_default {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sse_algorithm     = "AES256"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Replace <code>&lt;your_app_name&gt;</code> with your app name (no spaces, only underscores or letters)</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-71---configure-aws-profile">Step 7.1 - Configure AWS Profile<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-71---configure-aws-profile" class="hash-link" aria-label="Direct link to Step 7.1 - Configure AWS Profile" title="Direct link to Step 7.1 - Configure AWS Profile" translate="no">​</a></h3>
<p>Open or create file <code>~/.aws/credentials</code> and add (if not already there):</p>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">[myaws]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_access_key_id = &lt;your_access_key&gt;</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_secret_access_key = &lt;your_secret_key&gt;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-72---run-deployment">Step 7.2 - Run deployment<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-72---run-deployment" class="hash-link" aria-label="Direct link to Step 7.2 - Run deployment" title="Direct link to Step 7.2 - Run deployment" translate="no">​</a></h3>
<p>We will run first deployment from local machine to create S3 bucket for storing Terraform state. In other words this deployment will create resources needed for storing Terraform state in the cloud and runnign deployment from GitHub actions.</p>
<p>In <code>deploy</code> folder run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init</span><br></span></code></pre></div></div>
<p>Now run deployement:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Please note that this command might block ask you your <code>sudo</code> password to append <code>appserver.local</code> to <code>/etc/hosts</code> file.</p>
</blockquote>
<blockquote>
<p>👆 Please note that command might show errors about pushing images, this is fine because current deployment is done here only to setup S3 bucket for state migration before migrating to cloud.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-8---migrate-state-to-the-cloud">Step 8 - Migrate state to the cloud<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-8---migrate-state-to-the-cloud" class="hash-link" aria-label="Direct link to Step 8 - Migrate state to the cloud" title="Direct link to Step 8 - Migrate state to the cloud" translate="no">​</a></h2>
<p>First deployment had to create S3 bucket for storing Terraform state. Now we need to migrate the state to the cloud.</p>
<p>Add to the end of <code>main.tf</code>:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"># Configure the backend to use the S3 bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> backend "s3" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   bucket         = "&lt;your_app_name&gt;-terraform-state"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   key            = "state.tfstate"  # Define a specific path for the state file</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   region         = "us-east-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   profile        = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   use_lockfile   = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Replace <code>&lt;your_app_name&gt;</code> with your app name (no spaces, only underscores or letters).
Unfortunately we can't use variables, HashiCorp thinks it is too dangerous 😥</p>
</blockquote>
<p>Now run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init -migrate-state</span><br></span></code></pre></div></div>
<p>Now run test deployment:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>
<p>Now you can delete local <code>terraform.tfstate</code> file and <code>terraform.tfstate.backup</code> file as they are in the cloud now.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-9---cicd---github-actions">Step 9 - CI/CD - Github Actions<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-9---cicd---github-actions" class="hash-link" aria-label="Direct link to Step 9 - CI/CD - Github Actions" title="Direct link to Step 9 - CI/CD - Github Actions" translate="no">​</a></h2>
<p>Create file <code>.github/workflows/deploy.yml</code>:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.github/workflows/deploy.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Deploy myadmin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">run-name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.actor </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> builds myadmin 🚀</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">on</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">push</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">jobs</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">Explore-GitHub-Actions</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">runs-on</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> ubuntu</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">concurrency</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">group</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> build</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">group</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">cancel-in-progress</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#ae81ff">false</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">steps</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🎉 The job was automatically triggered by a $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.event_name </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> event."</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🐧 This job is now running on a $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> runner.os </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> server"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🔎 The name of your branch is $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.ref </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Check out repository code</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">uses</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> actions/checkout@v4</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Set up Terraform</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">uses</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> hashicorp/setup</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">terraform@v2</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">with</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">terraform_version</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> 1.10.1 </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Set up Docker Buildx</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">uses</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> docker/setup</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">buildx</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">action@v3</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Import SSH keys</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          mkdir -p deploy/.keys</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "$VAULT_SSH_PRIVATE_KEY" &gt; deploy/.keys/id_rsa</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "$VAULT_SSH_PUBLIC_KEY" &gt; deploy/.keys/id_rsa.pub</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          chmod 600 deploy/.keys/id_rsa*</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_SSH_PRIVATE_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_SSH_PRIVATE_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_SSH_PUBLIC_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_SSH_PUBLIC_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Setup AWS credentials</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          mkdir -p ~/.aws</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          cat &lt;&lt;EOL &gt; ~/.aws/credentials</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          [myaws]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          aws_access_key_id=${VAULT_AWS_ACCESS_KEY_ID}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          aws_secret_access_key=${VAULT_AWS_SECRET_ACCESS_KEY}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          EOL</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_AWS_ACCESS_KEY_ID</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_AWS_ACCESS_KEY_ID </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_AWS_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_AWS_SECRET_ACCESS_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Prepare env</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "ADMINFORTH_SECRET=$VAULT_ADMINFORTH_SECRET" &gt; deploy/.env.secrets.prod</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_ADMINFORTH_SECRET</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_ADMINFORTH_SECRET </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Terraform build</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          cd deploy</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          terraform init -reconfigure</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          # example of unlocking tf state if needed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          # terraform force-unlock fb397548-8697-ea93-ab80-128a4f508fdf --force</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          terraform plan -out=tfplan </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          terraform apply tfplan </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">                </span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🍏 This job's status is $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> job.status </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">."</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-10---add-secrets-to-github">Step 10 - Add secrets to GitHub<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#step-10---add-secrets-to-github" class="hash-link" aria-label="Direct link to Step 10 - Add secrets to GitHub" title="Direct link to Step 10 - Add secrets to GitHub" translate="no">​</a></h3>
<p>Go to your GitHub repository, then <code>Settings</code> -&gt; <code>Secrets</code> -&gt; <code>New repository secret</code> and add:</p>
<ul>
<li class=""><code>VAULT_AWS_ACCESS_KEY_ID</code> - your AWS access key</li>
<li class=""><code>VAULT_AWS_SECRET_ACCESS_KEY</code> - your AWS secret key</li>
<li class=""><code>VAULT_SSH_PRIVATE_KEY</code> - execute <code>cat deploy/.keys/id_rsa</code> and paste to GitHub secrets</li>
<li class=""><code>VAULT_SSH_PUBLIC_KEY</code> - execute <code>cat deploy/.keys/id_rsa.pub</code> and paste to GitHub secrets</li>
<li class=""><code>VAULT_ADMINFORTH_SECRET</code> - generate some random string and paste to GitHub secrets, e.g. <code>openssl rand -base64 32 | tr -d '\n'</code></li>
</ul>
<p>Now you can push your changes to GitHub and see how it will be deployed automatically.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="adding-secrets">Adding secrets<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#adding-secrets" class="hash-link" aria-label="Direct link to Adding secrets" title="Direct link to Adding secrets" translate="no">​</a></h3>
<p>Once you will have sensitive tokens/passwords in your apps you have to store them in a secure way.</p>
<p>Simplest way is to use GitHub secrets.</p>
<p>Let's imagine you have <code>OPENAI_API_KEY</code> which will be used one of AI-powered plugins of adminforth. We can't put this key to the code, so we have to store it in GitHub secrets.</p>
<p>Open your GitHub repository, then <code>Settings</code> -&gt; <code>Secrets</code> -&gt; <code>New repository secret</code> and add <code>VAULT_OPENAI_API_KEY</code> with your key.</p>
<p>Now open GitHub actions file and add it to the <code>env</code> section:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.github/workflows/deploy.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Prepare env</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "ADMINFORTH_SECRET=$VAULT_ADMINFORTH_SECRET" &gt; deploy/.env.secrets.prod</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "OPENAI_API_KEY=$VAULT_OPENAI_API_KEY" &gt;&gt; deploy/.env.secrets.prod</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_ADMINFORTH_SECRET</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_ADMINFORTH_SECRET </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_OPENAI_API_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_OPENAI_API_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>In the same way you can add any other secrets to your GitHub actions.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-to-connect-to-ec2-instance">How to connect to EC2 instance?<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#how-to-connect-to-ec2-instance" class="hash-link" aria-label="Direct link to How to connect to EC2 instance?" title="Direct link to How to connect to EC2 instance?" translate="no">​</a></h3>
<p>To connect to EC2 instance you can use SSH.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token builtin class-name" style="color:#e6db74">cd</span><span class="token plain"> deploy</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">ssh</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-i</span><span class="token plain"> ./.keys/id_rsa ubuntu@</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token plain">your_ec2_ip</span><span class="token operator" style="color:#66d9ef">&gt;</span><br></span></code></pre></div></div>
<p>IP address can be found in terminal output after terraform apply.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="out-of-space-on-ec2-instance-extend-ebs-volume">Out of space on EC2 instance? Extend EBS volume<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#out-of-space-on-ec2-instance-extend-ebs-volume" class="hash-link" aria-label="Direct link to Out of space on EC2 instance? Extend EBS volume" title="Direct link to Out of space on EC2 instance? Extend EBS volume" translate="no">​</a></h3>
<p>To upgrade EBS volume size you have to do next steps:</p>
<p>In <code>main.tf</code> file:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">  root_block_device {</span><br></span><span class="token-line code-block-diff-remove-line" style="color:#f8f8f2"><span class="token plain">    volume_size = 20 // Size in GB for root partition</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    volume_size = 40 // Size in GB for root partition</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_type = "gp2"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span></code></pre></div></div>
<p>And run build.</p>
<p>This will increase physical size of EBS volume, but you have to increase filesystem size too.</p>
<p>Login to EC2 instance:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">ssh</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-i</span><span class="token plain"> ./.keys/id_rsa ubuntu@</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token plain">your_ec2_ip</span><span class="token operator" style="color:#66d9ef">&gt;</span><br></span></code></pre></div></div>
<blockquote>
<p>You can find your EC2 IP in AWS console by visiting EC2 -&gt; Instances -&gt; Your instance -&gt; IPv4 Public IP</p>
</blockquote>
<p>Now run next commands:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">lsblk</span><br></span></code></pre></div></div>
<p>This would show something like this:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">loop0     </span><span class="token number" style="color:#ae81ff">7</span><span class="token plain">:0    </span><span class="token number" style="color:#ae81ff">0</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">99</span><span class="token plain">.4M  </span><span class="token number" style="color:#ae81ff">1</span><span class="token plain"> loop /snap/core/10908</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">nvme0n1 </span><span class="token number" style="color:#ae81ff">259</span><span class="token plain">:0    </span><span class="token number" style="color:#ae81ff">0</span><span class="token plain">   40G  </span><span class="token number" style="color:#ae81ff">0</span><span class="token plain"> disk</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">└─nvme0n1p1 </span><span class="token number" style="color:#ae81ff">259</span><span class="token plain">:1    </span><span class="token number" style="color:#ae81ff">0</span><span class="token plain">   20G  </span><span class="token number" style="color:#ae81ff">0</span><span class="token plain"> part /</span><br></span></code></pre></div></div>
<p>Here we see that <code>nvme0n1</code> is our disk and <code>nvme0n1p1</code> is our partition.</p>
<p>Now to extend partition run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> growpart /dev/nvme0n1 </span><span class="token number" style="color:#ae81ff">1</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> resize2fs /dev/nvme0n1p1</span><br></span></code></pre></div></div>
<p>This will extend partition to the full disk size. No reboot is needed.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="want-slack-notifications-about-build">Want slack notifications about build?<a href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/#want-slack-notifications-about-build" class="hash-link" aria-label="Direct link to Want slack notifications about build?" title="Direct link to Want slack notifications about build?" translate="no">​</a></h3>
<p>Create Slack channel and add <a href="https://slack.com/apps/A0F7YS25R-incoming-webhooks" target="_blank" rel="noopener noreferrer" class="">Slack app</a> to it.</p>
<p>Then create webhook URL and add it to GitHub secrets as <code>SLACK_WEBHOOK_URL</code>.</p>
<p>Add this steps to the end of your GitHub actions file:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.github/workflows/deploy.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Notify Slack on success</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">if</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> success()</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          curl -X POST -H 'Content-type: application/json' --data \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          "{\"text\": \"✅ *${{ github.actor }}* successfully built *${{ github.ref_name }}* with commit \\\"${{ github.event.head_commit.message }}\\\".\n:link: &lt;${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Build&gt; | :link: &lt;${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|View Commit&gt;\"}" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          $SLACK_WEBHOOK_URL </span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">SLACK_WEBHOOK_URL</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.SLACK_WEBHOOK_URL </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Notify Slack on failure</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">if</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> failure()</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          curl -X POST -H 'Content-type: application/json' --data \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          "{\"text\": \"❌ *${{ github.actor }}* failed to build *${{ github.ref_name }}* with commit \\\"${{ github.event.head_commit.message }}\\\".\n:link: &lt;${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Build&gt; | :link: &lt;${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|View Commit&gt;\"}" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          $SLACK_WEBHOOK_URL </span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">SLACK_WEBHOOK_URL</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.SLACK_WEBHOOK_URL </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="AWS" term="AWS"/>
        <category label="Terraform" term="Terraform"/>
        <category label="GitHub Actions" term="GitHub Actions"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Amazon EC2 Deployments with GitHub Actions, Terraform, Docker & Self-hosted Registry]]></title>
        <id>https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/</id>
        <link href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/"/>
        <updated>2025-02-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The ultimate step-by-step guide to cost-effective, build-time-efficient, and easy managable EC2 deployments using GitHub Actions, Terraform, Docker, and a self-hosted registry.]]></summary>
        <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/ga-tf-aws-fd1b40a236f46c0ffc8c09ef37abaf38.jpg" width="1200" height="630" class="img_ev3q"></p>
<p>This guide is a hackers extended addition of <a class="" href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/">Deploying AdminForth to EC2 with Amazon ECR</a>. The key difference in this post that we will not use Amazon ECR but self-host registry on EC2 itself. Automatically from terraform. And will see whether we will win something in terms of build time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="costs-for-amazon-ecr-vs-consts-for-self-hosted-registry-on-ec2">Costs for Amazon ECR vs consts for self-hosted registry on EC2<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#costs-for-amazon-ecr-vs-consts-for-self-hosted-registry-on-ec2" class="hash-link" aria-label="Direct link to Costs for Amazon ECR vs consts for self-hosted registry on EC2" title="Direct link to Costs for Amazon ECR vs consts for self-hosted registry on EC2" translate="no">​</a></h2>
<p>Most of AWS services are formed from EC2 prices plus some extra overhead for own cost. In same way, Amazon ECR pricing is pretty same.</p>
<table><thead><tr><th>Feature</th><th>Amazon ECR</th><th>Self-hosted registry on EC2</th></tr></thead><tbody><tr><td>Storage</td><td>$0.10 per GB/month</td><td>$0.10 per GB/month for gp2 EBS volume</td></tr><tr><td>Data transfer for egress</td><td>$0.09 per GB</td><td>$0.09 per GB</td></tr></tbody></table>
<p>So as you can see there is still no difference in terms of cost. However the approach in this system allows to replace Amazon EC2 with any other cloud provider which does not charge for egress traffic.</p>
<h1>Bechnmarking build time</h1>
<p>When I implmented this solution, I was curious whether it will be faster to build images on EC2 or on CI. So I did a little bit of testing.
First I used results form <a class="" href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/">deploying AdminForth to EC2 with Terraform without registry</a> where we built images on EC2. Then I did the same test but with self-hosted registry on EC2 and compared to <a class="" href="https://adminforth.dev/blog/compose-aws-ec2-ecr-terraform-github-actions/">deploying AdminForth to EC2 with Amazon ECR</a> where we built images on CI and pushed to Amazon ECR.</p>
<table><thead><tr><th>Feature</th><th>Without Registry (build directly on EC2)</th><th>With self-hosted registry</th><th>With Amazon ECR</th></tr></thead><tbody><tr><td>Initial build time*</td><td>3m 13.541s</td><td>2m 48.412s</td><td>3m 54s</td></tr><tr><td>Rebuild time (changed <code>index.ts</code>)*</td><td>0m 51.653s</td><td>0m42.131s</td><td>0m 54.120s</td></tr></tbody></table>
<sub>* All tests done from local machine (Intel(R) Core(TM) Ultra 9 185H, Docker Desktop/WSL2 64 GB RAM, 300Mbps up/down) up to working state</sub>
<p>So it indeed own self-hosted registry is faster then ECR and overall build time of pure AdminForth is faster then building on EC2. When ECR is slower then self-hosted registry, it is because of network speed.</p>
<h1>Chellenges when you build on CI</h1>
<h1>Registry authorization and traffic encryption</h1>
<p>Hosting custom CNCF registry, from other hand is a security responsibility.</p>
<p>If you don't protect it right, someone will be able to push any image to your registry and then pull it to your EC2 instance. This is a big security issue, so we have to protect our registry.</p>
<p>First of all we need to set some authorization to our registry so everyone who will push/pull images will be authorized. Here we have 2 options: HTTP basic auth and Client certificate auth. We will use first one as it is easier to setup. We will generate basic login and password automatically in terraform so no extra actions are needed from you.</p>
<p>But this is not enough. Basic auth is not encrypted, so someone can perform MITM attack and get your credentials. So we need to encrypt traffic between CI and registry. We can do it by using TLS certificates. So we will generate self-signed TLS certificates, and attach them to our registry.</p>
<p>Though the challenge is that we need to provide CA certificate to every daemon which will work with our registry. So we need to provide CA certificate to buildx daemon on CI, also if we want to do it from local machine, we need to provide CA certificate to local docker daemon.</p>
<h1>Practice - deploy setup</h1>
<p>Assume you have your AdminForth project in <code>myadmin</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1---dockerfile-and-dockerignore">Step 1 - Dockerfile and .dockerignore<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-1---dockerfile-and-dockerignore" class="hash-link" aria-label="Direct link to Step 1 - Dockerfile and .dockerignore" title="Direct link to Step 1 - Dockerfile and .dockerignore" translate="no">​</a></h2>
<p>This guide assumes you have created your AdminForth application with latest version of <code>adminforth create-app</code> command.
This command already creates a <code>Dockerfile</code> and <code>.dockerignore</code> for you, so you can use them as is.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2---composeyml">Step 2 - compose.yml<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-2---composeyml" class="hash-link" aria-label="Direct link to Step 2 - compose.yml" title="Direct link to Step 2 - compose.yml" translate="no">​</a></h2>
<p>create folder <code>deploy</code> and create file <code>compose.yml</code> inside:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/compose.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">services</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">traefik</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik:v2.5"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--api.insecure=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--providers.docker=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--entrypoints.web.address=:80"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"80:80"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"/var/run/docker.sock:/var/run/docker.sock:ro"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">myadmin</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> localhost</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">5000/myadmin</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">build</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">context</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> ../adminforth</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">app</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">tags</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> localhost</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">5000/myadmin</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">cache_from</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> type=registry</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">ref=localhost</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">5000/myadmin</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">cache</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">cache_to</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> type=registry</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">ref=localhost</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">5000/myadmin</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">cache</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">mode=max</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">compression=zstd</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">image</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">manifest=true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">oci</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">mediatypes=true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">pull_policy</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> always</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">restart</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> always</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">env_file</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> .env.secrets.prod</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> myadmin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">db</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">/code/db</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">labels</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.enable=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.routers.myadmin.rule=PathPrefix(`/`)"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.services.myadmin.loadbalancer.server.port=3500"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.routers.myadmin.priority=2"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  myadmin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">db</span><span class="token punctuation" style="color:#f8f8f2">:</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3---create-a-ssh-keypair">Step 3 - create a SSH keypair<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-3---create-a-ssh-keypair" class="hash-link" aria-label="Direct link to Step 3 - create a SSH keypair" title="Direct link to Step 3 - create a SSH keypair" translate="no">​</a></h2>
<p>Make sure you are still in <code>deploy</code> folder, run next command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">mkdir</span><span class="token plain"> .keys </span><span class="token operator" style="color:#66d9ef">&amp;&amp;</span><span class="token plain"> ssh-keygen </span><span class="token parameter variable" style="color:#f8f8f2">-f</span><span class="token plain"> .keys/id_rsa </span><span class="token parameter variable" style="color:#f8f8f2">-N</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">""</span><br></span></code></pre></div></div>
<p>Now it should create <code>deploy/.keys/id_rsa</code> and <code>deploy/.keys/id_rsa.pub</code> files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4---create-tls-certificates-to-encrypt-traffic-between-ci-and-registry">Step 4 - create TLS certificates to encrypt traffic between CI and registry<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-4---create-tls-certificates-to-encrypt-traffic-between-ci-and-registry" class="hash-link" aria-label="Direct link to Step 4 - create TLS certificates to encrypt traffic between CI and registry" title="Direct link to Step 4 - create TLS certificates to encrypt traffic between CI and registry" translate="no">​</a></h2>
<p>Make sure you are still in <code>deploy</code> folder, run next command:</p>
<p>Run next command to create TLS certificates:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">openssl req </span><span class="token parameter variable" style="color:#f8f8f2">-new</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-x509</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-days</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">3650</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-newkey</span><span class="token plain"> rsa:4096 </span><span class="token parameter variable" style="color:#f8f8f2">-nodes</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-keyout</span><span class="token plain"> .keys/ca.key </span><span class="token parameter variable" style="color:#f8f8f2">-subj</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"/CN=My Custom CA"</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-out</span><span class="token plain"> .keys/ca.pem</span><br></span></code></pre></div></div>
<p>This will create <code>deploy/.keys/ca.key</code> and <code>deploy/.keys/ca.pem</code> files.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5---gitignore-file">Step 5 - .gitignore file<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-5---gitignore-file" class="hash-link" aria-label="Direct link to Step 5 - .gitignore file" title="Direct link to Step 5 - .gitignore file" translate="no">​</a></h2>
<p>Create <code>deploy/.gitignore</code> file with next content:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">.terraform/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.keys/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfstate</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfstate.*</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfvars</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">tfplan</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.env.secrets.prod</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6---file-with-secrets-for-local-deploy">Step 6 - file with secrets for local deploy<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-6---file-with-secrets-for-local-deploy" class="hash-link" aria-label="Direct link to Step 6 - file with secrets for local deploy" title="Direct link to Step 6 - file with secrets for local deploy" translate="no">​</a></h2>
<p>Create file <code>deploy/.env.secrets.prod</code></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token assign-left variable" style="color:#f8f8f2">ADMINFORTH_SECRET</span><span class="token operator" style="color:#66d9ef">=</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token plain">your_secret</span><span class="token operator" style="color:#66d9ef">&gt;</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-7---main-terraform-file-maintf">Step 7 - main terraform file main.tf<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-7---main-terraform-file-maintf" class="hash-link" aria-label="Direct link to Step 7 - main terraform file main.tf" title="Direct link to Step 7 - main terraform file main.tf" translate="no">​</a></h2>
<p>First of all install Terraform as described here <a href="https://developer.hashicorp.com/terraform/install#linux" target="_blank" rel="noopener noreferrer" class="">terraform installation</a>.</p>
<p>Create file <code>main.tf</code> in <code>deploy</code> folder:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">locals {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app_name = "&lt;your_app_name&gt;"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  aws_region = "us-east-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">provider "aws" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  region = local.aws_region</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  profile = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_ami" "ubuntu_linux" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  most_recent = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  owners      = ["amazon"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "name"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_vpc" "default" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  default = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_eip" "eip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> domain = "vpc"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_eip_association" "eip_assoc" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> instance_id   = aws_instance.app_instance.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> allocation_id = aws_eip.eip.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_subnet" "default_subnet" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "vpc-id"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = [data.aws_vpc.default.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "default-for-az"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["true"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "availability-zone"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["${local.aws_region}a"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_security_group" "instance_sg" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name   = "${local.app_name}-instance-sg"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id = data.aws_vpc.default.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow HTTP"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow Docker registry"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 5000</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 5000</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # SSH</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow SSH"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  egress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow all outbound traffic"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_key_pair" "app_deployer" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name   = "terraform-deploy_${local.app_name}-key"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_instance" "app_instance" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ami                    = data.aws_ami.ubuntu_linux.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  instance_type          = "t3a.small"  # just change it to another type if you need, check https://instances.vantage.sh/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_id              = data.aws_subnet.default_subnet.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_security_group_ids = [aws_security_group.instance_sg.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name               = aws_key_pair.app_deployer.key_name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # prevent accidental termination of ec2 instance and data loss</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # if you will need to recreate the instance still (not sure why it can be?), you will need to remove this block manually by next command:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # &gt; terraform taint aws_instance.app_instance</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  lifecycle {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    prevent_destroy = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ignore_changes = [ami]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  root_block_device {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_size = 20 // Size in GB for root partition</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_type = "gp2"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Even if the instance is terminated, the volume will not be deleted, delete it manually if needed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    delete_on_termination = false</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  user_data = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    #!/bin/bash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get update</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get install ca-certificates curl</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo install -m 0755 -d /etc/apt/keyrings</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo chmod a+r /etc/apt/keyrings/docker.asc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Add the repository to Apt sources:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      $(. /etc/os-release &amp;&amp; echo "$VERSION_CODENAME") stable" | \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get update</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin screen</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl start docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl enable docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    usermod -a -G docker ubuntu</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo "done" &gt; /home/ubuntu/user_data_done</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Name = "${local.app_name}-instance"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "wait_for_user_data" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'Waiting for EC2 software install to finish...'",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "while [ ! -f /home/ubuntu/user_data_done ]; do echo '...'; sleep 2; done",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'EC2 software install finished.'"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("./.keys/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_instance.app_instance]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "setup_registry" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Generating secret for local registry"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sha256sum ./.keys/id_rsa | cut -d ' ' -f1 | tr -d '\n' &gt; ./.keys/registry.pure</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Creating htpasswd file for local registry"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker run --rm --entrypoint htpasswd httpd:2 -Bbn ci-user $(cat ./.keys/registry.pure) &gt; ./.keys/registry.htpasswd</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Generating server certificate for registry"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      openssl genrsa -out ./.keys/registry.key 4096</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "subjectAltName=DNS:appserver.local,DNS:localhost,IP:127.0.0.1" &gt; san.ext</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      openssl req -new -key ./.keys/registry.key -subj "/CN=appserver.local" -addext "$(cat san.ext)" -out ./.keys/registry.csr</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      openssl x509 -req -days 365 -CA ./.keys/ca.pem -CAkey ./.keys/ca.key -set_serial 01 -in ./.keys/registry.csr -extfile san.ext -out ./.keys/registry.crt </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Copying registry secret files to the instance"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      rsync -t -avz -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        ./.keys/registry.* ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/registry-auth</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [&lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # remove old registry if exists</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker rm -f registry</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # run new registry</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker run -d --network host \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --name registry \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --restart always \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        -v /home/ubuntu/registry-data:/var/lib/registry \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        -v /home/ubuntu/registry-auth:/auth\</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        -e "REGISTRY_AUTH=htpasswd" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        -e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/registry.htpasswd" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        -e "REGISTRY_HTTP_TLS_CERTIFICATE=/auth/registry.crt" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        -e "REGISTRY_HTTP_TLS_KEY=/auth/registry.key" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        registry:2</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("./.keys/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    always_run = 1 # change number to redeploy registry (if for some reason it was removed)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [null_resource.wait_for_user_data]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "sync_files_and_run" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # map appserver.local to the instance (in CI we don't know the IP, so have to use this mapping)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # so then in GA pipeline we will use </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      #  - name: Set up Docker Buildx</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      #   uses: docker/setup-buildx-action@v3</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      #   with:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      #     buildkitd-config-inline: |</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      #       [registry."appserver.local:5000"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      #         ca=["deploy/.keys/ca.pem"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      grep -q "appserver.local" /etc/hosts || echo "${aws_eip_association.eip_assoc.public_ip} appserver.local" | sudo tee -a /etc/hosts</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # hosts modification may take some time to apply</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sleep 5</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # generate buildx authorization</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sha256sum ./.keys/id_rsa | cut -d ' ' -f1 | tr -d '\n' &gt; ./.keys/registry.pure</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo '{"auths":{"appserver.local:5000":{"auth":"'$(echo -n "ci-user:$(cat ./.keys/registry.pure)" | base64 -w 0)'"}}}' &gt; ~/.docker/config.json</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Running build"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker buildx bake --progress=plain --push --allow=fs.read=.. -f compose.yml</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # compose temporarily it is not working https://github.com/docker/compose/issues/11072#issuecomment-1848974315</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # docker compose --progress=plain -p app -f ./compose.yml build --push</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # if you will change host, pleasee add -o StrictHostKeyChecking=no</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Copy files to the instance" </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      rsync -t -avz --mkpath -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --delete \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --exclude '.terraform' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --exclude '.keys' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        --exclude 'tfplan' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        . ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/deploy/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Run docker compose after files have been copied</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [&lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # login to docker registry</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      cat /home/ubuntu/registry-auth/registry.pure | docker login localhost:5000 -u ci-user --password-stdin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      cd /home/ubuntu/app/deploy</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      echo "Spinning up the app"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      docker compose --progress=plain -p app -f compose.yml up -d --remove-orphans</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # cleanup unused cache (run in background to not block terraform)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      screen -dm docker system prune -f</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      screen -dm docker exec registry registry garbage-collect /etc/docker/registry/config.yml --delete-untagged=true </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("./.keys/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Ensure the resource is triggered every time based on timestamp or file hash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    always_run = timestamp()</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_eip_association.eip_assoc, null_resource.setup_registry]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">output "instance_public_ip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  value = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">######### META, tf state ##############</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"># S3 bucket for storing Terraform state</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = "${local.app_name}-terraform-state"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  rule {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    status = "Enabled"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    id = "Keep only the latest version of the state file"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      prefix = ""</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    noncurrent_version_expiration {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      noncurrent_days = 30</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_versioning" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  versioning_configuration {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    status = "Enabled"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  rule {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    apply_server_side_encryption_by_default {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sse_algorithm     = "AES256"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Replace <code>&lt;your_app_name&gt;</code> with your app name (no spaces, only underscores or letters)</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-71---configure-aws-profile">Step 7.1 - Configure AWS Profile<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-71---configure-aws-profile" class="hash-link" aria-label="Direct link to Step 7.1 - Configure AWS Profile" title="Direct link to Step 7.1 - Configure AWS Profile" translate="no">​</a></h3>
<p>Open or create file <code>~/.aws/credentials</code> and add (if not already there):</p>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">[myaws]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_access_key_id = &lt;your_access_key&gt;</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_secret_access_key = &lt;your_secret_key&gt;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-72---run-deployment">Step 7.2 - Run deployment<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-72---run-deployment" class="hash-link" aria-label="Direct link to Step 7.2 - Run deployment" title="Direct link to Step 7.2 - Run deployment" translate="no">​</a></h3>
<p>We will run first deployment from local machine to create S3 bucket for storing Terraform state. In other words this deployment will create resources needed for storing Terraform state in the cloud and runnign deployment from GitHub actions.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init</span><br></span></code></pre></div></div>
<p>Now run deployement:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Please note that this command might block ask you your <code>sudo</code> password to append <code>appserver.local</code> to <code>/etc/hosts</code> file.</p>
</blockquote>
<blockquote>
<p>👆 Please note that command might show errors about pushing images, this is fine because current deployment is done here only to setup S3 bucket for state migration before migrating to cloud.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-8---migrate-state-to-the-cloud">Step 8 - Migrate state to the cloud<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-8---migrate-state-to-the-cloud" class="hash-link" aria-label="Direct link to Step 8 - Migrate state to the cloud" title="Direct link to Step 8 - Migrate state to the cloud" translate="no">​</a></h2>
<p>First deployment had to create S3 bucket for storing Terraform state. Now we need to migrate the state to the cloud.</p>
<p>Add to the end of <code>main.tf</code>:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"># Configure the backend to use the S3 bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> backend "s3" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   bucket         = "&lt;your_app_name&gt;-terraform-state"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   key            = "state.tfstate"  # Define a specific path for the state file</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   region         = "us-east-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   profile        = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   use_lockfile   = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Replace <code>&lt;your_app_name&gt;</code> with your app name (no spaces, only underscores or letters).
Unfortunately we can't use variables, HashiCorp thinks it is too dangerous 😥</p>
</blockquote>
<p>Now run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init -migrate-state</span><br></span></code></pre></div></div>
<p>Now run test deployment:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>
<p>Now you can delete local <code>terraform.tfstate</code> file and <code>terraform.tfstate.backup</code> file as they are in the cloud now.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-9---cicd---github-actions">Step 9 - CI/CD - Github Actions<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-9---cicd---github-actions" class="hash-link" aria-label="Direct link to Step 9 - CI/CD - Github Actions" title="Direct link to Step 9 - CI/CD - Github Actions" translate="no">​</a></h2>
<p>Create file <code>.github/workflows/deploy.yml</code>:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.github/workflows/deploy.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Deploy myadmin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">run-name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.actor </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> builds myadmin 🚀</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">on</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">push</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">jobs</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">Explore-GitHub-Actions</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">runs-on</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> ubuntu</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">concurrency</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">group</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> build</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">group</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">cancel-in-progress</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#ae81ff">false</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">steps</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🎉 The job was automatically triggered by a $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.event_name </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> event."</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🐧 This job is now running on a $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> runner.os </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> server"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🔎 The name of your branch is $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.ref </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Check out repository code</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">uses</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> actions/checkout@v4</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Set up Terraform</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">uses</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> hashicorp/setup</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">terraform@v2</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">with</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">terraform_version</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> 1.10.1 </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Import Registry CA</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          mkdir -p deploy/.keys</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "$VAULT_REGISTRY_CA_PEM" &gt; deploy/.keys/ca.pem</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "$VAULT_REGISTRY_CA_KEY" &gt; deploy/.keys/ca.key</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_REGISTRY_CA_PEM</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_REGISTRY_CA_PEM </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_REGISTRY_CA_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_REGISTRY_CA_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Set up Docker Buildx</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">uses</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> docker/setup</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">buildx</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">action@v3</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">with</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">buildkitd-config-inline</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">            [registry."appserver.local:5000"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">              ca=["deploy/.keys/ca.pem"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">              </span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token comment" style="color:#8292a2;font-style:italic"># use host network for resolving appserver.local</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">driver-opts</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> network=host</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Import registry SSH keys</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          mkdir -p deploy/.keys</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "$VAULT_SSH_PRIVATE_KEY" &gt; deploy/.keys/id_rsa</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "$VAULT_SSH_PUBLIC_KEY" &gt; deploy/.keys/id_rsa.pub</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          chmod 600 deploy/.keys/id_rsa*</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_SSH_PRIVATE_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_SSH_PRIVATE_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_SSH_PUBLIC_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_SSH_PUBLIC_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Setup AWS credentials</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          mkdir -p ~/.aws</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          cat &lt;&lt;EOL &gt; ~/.aws/credentials</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          [myaws]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          aws_access_key_id=${VAULT_AWS_ACCESS_KEY_ID}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          aws_secret_access_key=${VAULT_AWS_SECRET_ACCESS_KEY}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          EOL</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_AWS_ACCESS_KEY_ID</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_AWS_ACCESS_KEY_ID </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_AWS_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_AWS_SECRET_ACCESS_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Prepare env</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "ADMINFORTH_SECRET=$VAULT_ADMINFORTH_SECRET" &gt; deploy/.env.secrets.prod</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_ADMINFORTH_SECRET</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_ADMINFORTH_SECRET </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Terraform build</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          cd deploy</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          terraform init -reconfigure</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          # example of unlocking tf state if needed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          # terraform force-unlock fb397548-8697-ea93-ab80-128a4f508fdf --force</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          terraform plan -out=tfplan </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          terraform apply tfplan </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">                </span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🍏 This job's status is $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> job.status </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">."</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-81---add-secrets-to-github">Step 8.1 - Add secrets to GitHub<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#step-81---add-secrets-to-github" class="hash-link" aria-label="Direct link to Step 8.1 - Add secrets to GitHub" title="Direct link to Step 8.1 - Add secrets to GitHub" translate="no">​</a></h3>
<p>Go to your GitHub repository, then <code>Settings</code> -&gt; <code>Secrets</code> -&gt; <code>New repository secret</code> and add:</p>
<ul>
<li class=""><code>VAULT_AWS_ACCESS_KEY_ID</code> - your AWS access key</li>
<li class=""><code>VAULT_AWS_SECRET_ACCESS_KEY</code> - your AWS secret key</li>
<li class=""><code>VAULT_SSH_PRIVATE_KEY</code> - execute <code>cat ~/.ssh/id_rsa</code> and paste to GitHub secrets</li>
<li class=""><code>VAULT_SSH_PUBLIC_KEY</code> - execute <code>cat ~/.ssh/id_rsa.pub</code> and paste to GitHub secrets</li>
<li class=""><code>VAULT_REGISTRY_CA_PEM</code> - execute <code>cat deploy/.keys/ca.pem</code> and paste to GitHub secrets</li>
<li class=""><code>VAULT_REGISTRY_CA_KEY</code> - execute <code>cat deploy/.keys/ca.key</code> and paste to GitHub secrets</li>
<li class=""><code>VAULT_ADMINFORTH_SECRET</code> - generate some random string and paste to GitHub secrets, e.g. <code>openssl rand -base64 32 | tr -d '\n'</code></li>
</ul>
<p>Now you can push your changes to GitHub and see how it will be deployed automatically.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="adding-secrets">Adding secrets<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#adding-secrets" class="hash-link" aria-label="Direct link to Adding secrets" title="Direct link to Adding secrets" translate="no">​</a></h3>
<p>Once you will have sensitive tokens/passwords in your apps you have to store them in a secure way.</p>
<p>Simplest way is to use GitHub secrets.</p>
<p>Let's imagine you have <code>OPENAI_API_KEY</code> which will be used one of AI-powered plugins of adminforth. We can't put this key to the code, so we have to store it in GitHub secrets.</p>
<p>Open your GitHub repository, then <code>Settings</code> -&gt; <code>Secrets</code> -&gt; <code>New repository secret</code> and add <code>VAULT_OPENAI_API_KEY</code> with your key.</p>
<p>Now open GitHub actions file and add it to the <code>env</code> section:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.github/workflows/deploy.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Prepare env</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "ADMINFORTH_SECRET=$VAULT_ADMINFORTH_SECRET" &gt; deploy/.env.secrets.prod</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "OPENAI_API_KEY=$VAULT_OPENAI_API_KEY" &gt;&gt; deploy/.env.secrets.prod</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_ADMINFORTH_SECRET</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_ADMINFORTH_SECRET </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_OPENAI_API_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_OPENAI_API_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>In the same way you can add any other secrets to your GitHub actions.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="want-to-run-builds-from-your-local-machine">Want to run builds from your local machine?<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#want-to-run-builds-from-your-local-machine" class="hash-link" aria-label="Direct link to Want to run builds from your local machine?" title="Direct link to Want to run builds from your local machine?" translate="no">​</a></h3>
<p>This guide originally was created to run full builds from GitHub actions only, so out of the box it will fail to push images to registry from your local machine.</p>
<p>But for debug purporses you can run it from your local machine too with some addition steps.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-you-need-to-make-local-docker-buildx-builder-to-trust-self-signed-tls-certificate">1. You need to make local Docker buildx builder to trust self-signed TLS certificate<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#1-you-need-to-make-local-docker-buildx-builder-to-trust-self-signed-tls-certificate" class="hash-link" aria-label="Direct link to 1. You need to make local Docker buildx builder to trust self-signed TLS certificate" title="Direct link to 1. You need to make local Docker buildx builder to trust self-signed TLS certificate" translate="no">​</a></h4>
<p>Create folder <code>deploy/.local</code> and create next files:</p>
<div class="language-toml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-toml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">[registry."appserver.local:5000"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  insecure = false</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ca = ["../.keys/ca.pem"]</span><br></span></code></pre></div></div>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token shebang important">#!/bin/bash</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token builtin class-name" style="color:#e6db74">cd</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"</span><span class="token string variable" style="color:#f8f8f2">$(</span><span class="token string variable function" style="color:#e6db74">dirname</span><span class="token string variable" style="color:#f8f8f2"> </span><span class="token string variable string" style="color:#a6e22e">"</span><span class="token string variable string variable" style="color:#f8f8f2">$0</span><span class="token string variable string" style="color:#a6e22e">"</span><span class="token string variable" style="color:#f8f8f2">)</span><span class="token string" style="color:#a6e22e">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">docker</span><span class="token plain"> buildx </span><span class="token function" style="color:#e6db74">rm</span><span class="token plain"> mybuilder </span><span class="token operator" style="color:#66d9ef">||</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">docker</span><span class="token plain"> buildx create </span><span class="token parameter variable" style="color:#f8f8f2">--name</span><span class="token plain"> mybuilder </span><span class="token parameter variable" style="color:#f8f8f2">--driver</span><span class="token plain"> docker-container   </span><span class="token parameter variable" style="color:#f8f8f2">--use</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">--config</span><span class="token plain"> ./buildkitd.toml</span><br></span></code></pre></div></div>
<p>Now create builder:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">bash</span><span class="token plain"> .local/create-builder.sh</span><br></span></code></pre></div></div>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-you-need-to-deliver-same-secrets-from-local-machine-as-from-ci-vault">2. You need to deliver same secrets from local machine as from CI vault<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#2-you-need-to-deliver-same-secrets-from-local-machine-as-from-ci-vault" class="hash-link" aria-label="Direct link to 2. You need to deliver same secrets from local machine as from CI vault" title="Direct link to 2. You need to deliver same secrets from local machine as from CI vault" translate="no">​</a></h4>
<p>Create file <code>deploy/.env.secrets.prod</code> with next content:</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token assign-left variable" style="color:#f8f8f2">ADMINFORTH_SECRET</span><span class="token operator" style="color:#66d9ef">=</span><span class="token operator" style="color:#66d9ef">&lt;</span><span class="token plain">your secret</span><span class="token operator" style="color:#66d9ef">&gt;</span><br></span></code></pre></div></div>
<p>Please note that if you are running builds both from GA and local, the <code>ADMINFORTH_SECRET</code> should much to GA secret. Otherwise all existing users will be logged out.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-you-need-to-add-appserverlocal-to-your-hosts-file-windowswsl-only">2. You need to add app.server.local to your hosts file (Windows/WSL only)<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#2-you-need-to-add-appserverlocal-to-your-hosts-file-windowswsl-only" class="hash-link" aria-label="Direct link to 2. You need to add app.server.local to your hosts file (Windows/WSL only)" title="Direct link to 2. You need to add app.server.local to your hosts file (Windows/WSL only)" translate="no">​</a></h4>
<blockquote>
<p>This step is not needed on Linux / Mac because teraform provisioner will autiomatically add it to <code>/etc/hosts</code> file.
However in WSL we can't modify Windows native hosts file, so we need to do it manually.</p>
</blockquote>
<p>In power shell run</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">Start-Process notepad "C:\Windows\System32\drivers\etc\hosts" -Verb runAs</span><br></span></code></pre></div></div>
<p>Check your public IP in Terraform output and add</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">&lt;your public ip&gt; appserver.local</span><br></span></code></pre></div></div>
<blockquote>
<p>Bad news is that instance public IP will be known only after first run, so some steps would fail because there will be no hosts mapping. However since EC2 provisioning takes some time it is even possible to copy IP from terminal and inser it to hosts file from first run 🤪</p>
</blockquote>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-using-local-build-from-multiple-projects">3. Using local build from multiple projects<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions-registry/#3-using-local-build-from-multiple-projects" class="hash-link" aria-label="Direct link to 3. Using local build from multiple projects" title="Direct link to 3. Using local build from multiple projects" translate="no">​</a></h4>
<p>The easiest way would be probably to rename <code>appserver.local</code> to unique name for each project.</p>
<p>Then you can put all certificate mappings to a <code>buildkitd.toml</code> and move it along with <code>create-builder.sh</code> script to a common folder, e.g. home</p>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="AWS" term="AWS"/>
        <category label="Terraform" term="Terraform"/>
        <category label="GitHub Actions" term="GitHub Actions"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[How I Open-Sourced My Secret Access Tokens from GitHub, Slack, and NPM — and Who Actually Cares]]></title>
        <id>https://adminforth.dev/blog/how-i-opensourced-my-secret-tokens/</id>
        <link href="https://adminforth.dev/blog/how-i-opensourced-my-secret-tokens/"/>
        <updated>2025-01-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Our framework has a CI pipeline that runs npm run build, publishes the package to NPM (npm publish), and creates a new release on GitHub. It also sends a notification about the release to a Slack webhook for our team.]]></summary>
        <content type="html"><![CDATA[<p>Our framework has a CI pipeline that runs <code>npm run build</code>, publishes the package to NPM (<code>npm publish</code>), and creates a new release on GitHub. It also sends a notification about the release to a Slack webhook for our team.</p>
<p>Secrets for these services were stored in our CI’s built-in Vault (we are running a self-hosted Woodpecker CI).</p>
<p>Recently, while moving plugins to separate repositories, I decided to try <a href="https://infisical.com/" target="_blank" rel="noopener noreferrer" class="">Infisical</a> for centralized secrets management instead of the internal CI Vault. Infisical provides a self-hosted open-source solution, has a well-organized UI, and offers better access control than our CI Vault. It was important to me that I could reuse secrets across different repositories without copying them every time I created a new plugin.</p>
<p>Here’s what I did:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.woodpecker.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">steps</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">init-secrets</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">when</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">event</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> push</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> infisical/cli</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">environment</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">INFISICAL_TOKEN</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">from_secret</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> VAULT_TOKEN</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">commands</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> infisical export </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">domain https</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">//vault.devforth.io/api </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">format=dotenv</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">export </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">env="prod" </span><span class="token punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"> .vault.env</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">secrets</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> VAULT_TOKEN</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">release</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> node</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token number" style="color:#ae81ff">20</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">when</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">event</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> push</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">commands</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> export $(cat .vault.env </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token plain"> xargs)</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> cd adminforth</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> npm clean</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">install</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> npm run build</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> npm audit signatures</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token comment" style="color:#8292a2;font-style:italic"># does publish to npm, creates release on github, and sends notification to slack webhook</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> npx semantic</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">release </span><br></span><span class="token-line code-block-diff-remove-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">secrets</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-remove-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> VAULT_NPM_TOKEN</span><br></span><span class="token-line code-block-diff-remove-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> VAULT_GITHUB_TOKEN</span><br></span><span class="token-line code-block-diff-remove-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> VAULT_SLACK_TOKEN</span><br></span></code></pre></div></div>
<p>Pretty dumb method to export secrets to the <code>.vault.env</code> file, but it was late evening, and I didn’t want to spend much time on it at the start.</p>
<p>I made the first push, and everything worked fine on the first attempt. I was happy.</p>
<p>Then I started adding the same code to the first plugin, and the plugin build failed with a very unexpected error.</p>
<p>It said that my NPM token was invalid. I was surprised and started printing the environment variables to see what was wrong (printing environment variables to the build log is a pretty bad practice and is the last thing you want to do, but I knew it was an internal CI, and the project was private).</p>
<p>I saw that my NPM token was still in the environment variables and was the same.</p>
<p>I went back to the first repository and retried the build. It failed with the same error.</p>
<p>I went to NPM and found out that the token had disappeared entirely from the list. I was shocked and recreated it.</p>
<p>On the next build, I discovered that the Slack webhook was also not working. However, GitHub releases were created without issues in both repositories.</p>
<p>Then I noticed an email push notification from Slack titled "Notification about invalidated webhook URLs."</p>
<p><img decoding="async" loading="lazy" alt="Slack Notification about invalidated webhook URLs" src="https://adminforth.dev/assets/images/image-d15614a6703b6388201dc9a5dee77da7.png" width="1606" height="1634" class="img_ev3q"></p>
<p>This was the moment I realized that <code>npm publish</code> had simply taken my <code>.vault.env</code> file and published it to NPM.</p>
<p>Shortly after, I noticed a recent email from NPM titled "Granular access token deleted."</p>
<p><img decoding="async" loading="lazy" alt="npm Granular access token deleted" src="https://adminforth.dev/assets/images/image-1-f41049dc26b1e0741b6a167bf224d0e2.png" width="1722" height="1526" class="img_ev3q"></p>
<p>The next thing I did was revoke all tokens, including the GitHub token, which still worked, and unpublish all packages from NPM (though they might still be cloned by some caches/aggregators/archivers).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="github">GitHub<a href="https://adminforth.dev/blog/how-i-opensourced-my-secret-tokens/#github" class="hash-link" aria-label="Direct link to GitHub" title="Direct link to GitHub" translate="no">​</a></h2>
<p>GitHub was not able to recognize that the token had been leaked to an NPM package and revoke it. Although they do a pretty good job when you push other vendors’ secrets to a GitHub repository, it seems they don’t check NPM sources.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="npm">NPM<a href="https://adminforth.dev/blog/how-i-opensourced-my-secret-tokens/#npm" class="hash-link" aria-label="Direct link to NPM" title="Direct link to NPM" translate="no">​</a></h2>
<p>NPM detected that the NPM token was published to their registry and revoked it. However, it was hard to understand why—it was simply deleted. They sent an email, but it did not explain why the token was deleted or specify the source of the leak. Showing an error in the tokens list on the NPM website would have been the best option.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="slack">Slack<a href="https://adminforth.dev/blog/how-i-opensourced-my-secret-tokens/#slack" class="hash-link" aria-label="Direct link to Slack" title="Direct link to Slack" translate="no">​</a></h2>
<p>I was surprised, but Slack did a great job. They monitor NPM (I don’t think they monitor the whole NPM registry; there’s probably some interesting technology behind it). They detected that the NPM token was published to the registry and invalidated it. They sent an email with a clear explanation of why it was invalidated and what steps to take next.</p>
<h1>Conclusion</h1>
<p>We can talk a lot about bad programming practices, but the main takeaway is that we are human. And humans still make mistakes.<br>
<!-- -->It makes a lot of sense to monitor for such human errors.</p>
<p>In my case, NPM and Slack saved me from a potential security breach. Without their intervention, I would have learned about the issue only when someone used my tokens for malicious purposes.<br>
<!-- -->GitHub didn’t detect or revoke the token, and many other services wouldn’t have done so either.</p>
<p>Here are some common recommendations I learned from this experience:</p>
<ul>
<li class="">Try to limit token access as much as possible to only the required granularity. Even if something is leaked, it won’t cause much harm. Don’t grant access to all resources/packages/repos unless it’s necessary.</li>
<li class="">Check what you publish, especially when making changes to your build pipeline. I missed the fact that the <code>.env</code> file was being published.</li>
<li class="">Appreciate services that monitor for leaks and respond to them. They can save you from potential security breaches.</li>
</ul>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why manual Release Notes and Versions are a chaos and how to fix it]]></title>
        <id>https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/</id>
        <link href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/"/>
        <updated>2025-01-19T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn what profits you can get from automatic versioning and learn how simply you can configure it!]]></summary>
        <content type="html"><![CDATA[<p>I have a feeling that after first ~600 versions of Adminforth we faced all possible issues with manual versioning and release notes.</p>
<p>Manual versioning and CHANGELOG.md is unreliable as human beings are. It is pretty easy to forget it with relevant information, forget to include some changes, forget to push it to GitHub, push it at wrong time, and many more things.</p>
<p>That is why we decided to move the idea of generating versions, and GitHub releases from git commit messages using great tool called <a href="https://semantic-release.gitbook.io/semantic-release/usage/configuration" target="_blank" rel="noopener noreferrer" class="">semantic-release</a>.</p>
<p>In this post I will explain why we did a transition from manual releases to automatic, what profits we got from it, and also will show you simple example how to do it in your project!</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="prehistory-and-issues">Prehistory and issues<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#prehistory-and-issues" class="hash-link" aria-label="Direct link to Prehistory and issues" title="Direct link to Prehistory and issues" translate="no">​</a></h2>
<p>Before 1.6.0 AdminForth was using manual CHANGELOG.md.</p>
<p>During development we were reviwing PRs, merged them all to <code>main</code> branch, pulled to local machine and there did manually npm release which also created a git tag and pushed it to GitHub.</p>
<p>We were constantly releasing to <code>next</code> pre-release version from <code>main</code> and used <code>next</code> internally on our projets for testing.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-npm-pre-release-version">What is npm pre-release version?<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#what-is-npm-pre-release-version" class="hash-link" aria-label="Direct link to What is npm pre-release version?" title="Direct link to What is npm pre-release version?" translate="no">​</a></h3>
<p>If you are not familiar with pre-release versions, I can explain it on simple example.</p>
<p>If you have version <code>1.2.3</code> in your <code>package.json</code> and you run <code>npm version patch</code> it will bump version to <code>1.2.4</code>. If you run it again it will bump version to <code>1.2.5</code>.
If you have version <code>1.2.3</code> and run <code>npm version prerelease --preid=next</code> it will bump version to <code>1.2.4-next.0</code>. If you run it again it will bump version to <code>1.2.4-next.1</code>. If you run <code>npm version patch</code> now it will release version <code>1.2.4</code>, hoever if at last step you run <code>npm version minor</code> it would release version <code>1.3.0</code>.</p>
<p>Please pay attention that <code>npm</code> is pretty smart and aligned with normal software release cycle – once you run <code>npm version pre-release</code> on stable version, it understands that you start working on new version and will bump patch version. Once you run <code>npm version patch</code> at pre-release version, it will release it as stable version without bumping patch number.</p>
<p>This is very useful because we can collect features and fixes in <code>next</code> version without releasing them to <code>latest</code> version, so users who do <code>npm install adminforth</code> will not get new experimental features and fixes, but thouse who want to test them early can do <code>npm install adminforth@next</code>. Our team also using <code>next</code> for commercial projects to test new features so in the end of the day we have more stable <code>latest</code> version.</p>
<p>In new automatic release process we preserved this approach, but made it in separate git branches.</p>
<p>Once we collected enough features and fixes in <code>next</code>, we were doing a release to <code>latest</code> version, and at this time we did release documentation.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="issue-one-easy-to-forget-add-something-to-changelogmd">Issue one: easy to forget add something to CHANGELOG.md<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#issue-one-easy-to-forget-add-something-to-changelogmd" class="hash-link" aria-label="Direct link to Issue one: easy to forget add something to CHANGELOG.md" title="Direct link to Issue one: easy to forget add something to CHANGELOG.md" translate="no">​</a></h3>
<p>So when I merged PRs to <code>main</code> branch, I had to check commits and write them to CHANGELOG.md.</p>
<p>At this point I wasted time to understand how to call the change in CHANGELOG.md, and categorize it as <code>Added</code>, <code>Changed</code>, <code>Fixed</code>, <code>Removed</code>, <code>Security</code>.</p>
<p>Also there was a chance that I will skip some commit, or will understand wrong what it was about.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="issue-two-changelogmd-might-be-forgot-to-be-changed-before-release-as-all">Issue two: CHANGELOG.md might be forgot to be changed before release as all<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#issue-two-changelogmd-might-be-forgot-to-be-changed-before-release-as-all" class="hash-link" aria-label="Direct link to Issue two: CHANGELOG.md might be forgot to be changed before release as all" title="Direct link to Issue two: CHANGELOG.md might be forgot to be changed before release as all" translate="no">​</a></h3>
<p>There was a chance that I will forget to update <code>CHANGELOG.md</code> at all. I merged PRs, I am hurry and doing release.</p>
<p>Sometimes I will soon understand that I forgot to update it and will push it to GitHub. It will born another dump commit message like "Update a CHANGELOG.md".</p>
<p>Also I am not sure that users are looking into <code>main</code> branch to see CHANGELOG, probably they see it in npmjs.com.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="issue-three-lack-of-github-releases-or-need-to-maintain-both">Issue three: lack of GitHub releases or need to maintain both<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#issue-three-lack-of-github-releases-or-need-to-maintain-both" class="hash-link" aria-label="Direct link to Issue three: lack of GitHub releases or need to maintain both" title="Direct link to Issue three: lack of GitHub releases or need to maintain both" translate="no">​</a></h3>
<p>We did not have GitHub releases at all at that time. We had only tags. And tags were applied only after release.</p>
<p>But honestly, when I am by myself working with some pacakge, first try to find GitHub releases, though CHANGELOG.md.</p>
<p>So if I would add GitHub releases, I would have to do a lot of clicks or would need some script / CLI to create release notes. This process would have similar issues as with CHANGELOG.md.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="issue-four-manual-tags">Issue four: manual tags<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#issue-four-manual-tags" class="hash-link" aria-label="Direct link to Issue four: manual tags" title="Direct link to Issue four: manual tags" translate="no">​</a></h3>
<p>Since release was manual from my PC there was a chance that I will do some minor fix, will forget to commit it, then will do release and release script will add tag to previous, irrelevant commit.</p>
<p>In such projects with manual tags, there is only one reliabile way for package user to to check what is difference in source code between two versions: using some tool like <a href="https://npmdiff.dev/" target="_blank" rel="noopener noreferrer" class="">https://npmdiff.dev/</a> and not rely on git and CHANGELOG</p>
<p>Since git tags were applied only after release and there again was a chance that I will forget to push them to GitHub with a release.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="issue-five-dump-commits">Issue five: dump commits<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#issue-five-dump-commits" class="hash-link" aria-label="Direct link to Issue five: dump commits" title="Direct link to Issue five: dump commits" translate="no">​</a></h3>
<p>Since with every manual release we updated CHANGELOG.md and updated version in <code>pacakge.json</code> every time, we had to do a commit.</p>
<p>So it borned a lot of dump commits like "Update CHANGELOG.md", "Update version to 1.2.3", "Update version to 1.2.4", "Update version to 1.2.5".</p>
<p>Sometime it wass forgot to be commited separately and was commited with other changes in some fix which is even worsen.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="issue-six-releae-delay-and-bus-factor">Issue six: releae delay and bus-factor<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#issue-six-releae-delay-and-bus-factor" class="hash-link" aria-label="Direct link to Issue six: releae delay and bus-factor" title="Direct link to Issue six: releae delay and bus-factor" translate="no">​</a></h3>
<p>So if I was busy new features were waiting for release, becuase only I had access to do releases.</p>
<p>Sonner I passed access to some of my colleagues to do releases but from time to time it caused state desync and release issues between our PCs if someone forgot to push something to GitHub after release.</p>
<p>Even couple of release contrubuters are onboard, it still takes a time to do all the stuff with Changelog, version, tags, etc, so it delays release.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="issue-seven-lack-of-pre-release-versions-changes">Issue seven: lack of pre-release versions changes<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#issue-seven-lack-of-pre-release-versions-changes" class="hash-link" aria-label="Direct link to Issue seven: lack of pre-release versions changes" title="Direct link to Issue seven: lack of pre-release versions changes" translate="no">​</a></h3>
<p>While we releasing to <code>next</code> we added items under one version in CHANGELOG for simplicity. It would be another extra time to add every <code>next</code> version in CHANGELOG and describe whole markdown for it.</p>
<p>So user was not able to distinguish <code>1.5.0-next.0</code> from <code>1.5.0-next.1</code> in CHANGELOG, which was not an issue for <code>latest</code> users but issue for <code>next</code> users.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="semantic-release-what-is-it-and-how-it-works">Semantic-release: what is it and how it works<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#semantic-release-what-is-it-and-how-it-works" class="hash-link" aria-label="Direct link to Semantic-release: what is it and how it works" title="Direct link to Semantic-release: what is it and how it works" translate="no">​</a></h2>
<p><code>semantic-release</code> is a tool that solved all issues above. Basically it is a CLI tool that you run on your CI/CD server on every push to your repository.</p>
<p>So on every push it analyzes commit messages from previous release and does next things:</p>
<ul>
<li class="">It understands what type of release it should do: major, minor, patch, pre-release depending on commit messages. E.g. if you have a commit message <code>feat: add new feature</code> it will do a minor release, if you have <code>fix: fix bug</code> it will do a patch release.</li>
<li class="">It reads previous version from git tags and does a new version based on type of release. So it does not edit <code>package.json</code> file.</li>
<li class="">It generates a GitHub tag and release notes based on commit messages. So you do not need to write CHANGELOG.md anymore.</li>
<li class="">It publishes a new version to npmjs.com. So you do not need to do <code>npm publish</code> anymore.</li>
<li class="">It is capable to release to <code>next</code> channel from separate <code>next</code> branch without version bumping. So you can collect features and fixes in <code>next</code> without releasing them to <code>latest</code>.</li>
<li class="">It has plugins, for example to send Slack notifications about releases.</li>
</ul>
<p>The sweet thing that it is all executed on CI/CD server, so you do not need to do anything manually.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="ussage-example">Ussage example<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#ussage-example" class="hash-link" aria-label="Direct link to Ussage example" title="Direct link to Ussage example" translate="no">​</a></h2>
<p>I will show a flow on empty fake small project to not overcomplicate things.</p>
<p>This will allow you to apply it to your project once you ready.</p>
<p>We will use minimal typescript package with <code>npm</code> and <code>semantic-release</code> to show how it works.</p>
<p>First init new git repository:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token builtin class-name" style="color:#e6db74">echo</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"# test-sem-release"</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">&gt;&gt;</span><span class="token plain"> README.md</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">git</span><span class="token plain"> init</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">git</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">add</span><span class="token plain"> README.md</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">git</span><span class="token plain"> commit </span><span class="token parameter variable" style="color:#f8f8f2">-m</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"first commit"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">git</span><span class="token plain"> branch </span><span class="token parameter variable" style="color:#f8f8f2">-M</span><span class="token plain"> main</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">git</span><span class="token plain"> remote </span><span class="token function" style="color:#e6db74">add</span><span class="token plain"> origin git@github.com:devforth/test-sem-release.git</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">git</span><span class="token plain"> push </span><span class="token parameter variable" style="color:#f8f8f2">-u</span><span class="token plain"> origin main</span><br></span></code></pre></div></div>
<p>Now lets init new <code>npm package</code>:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> init </span><span class="token parameter variable" style="color:#f8f8f2">-y</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">install</span><span class="token plain"> typescript --save-dev</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">npx tsc </span><span class="token parameter variable" style="color:#f8f8f2">--init</span><br></span></code></pre></div></div>
<p>Create a file <code>index.ts</code>:</p>
<div class="language-typescript codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">index.ts</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-typescript codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> greet </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">Hello, </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">name</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">!</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">;</span><br></span></code></pre></div></div>
<p>In <code>package.json</code> add:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"name"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"@devforth/test-sem-release"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"publishConfig"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token property" style="color:#f92672">"access"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"public"</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"version"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"1.0.0"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"main"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"index.js"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"scripts"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token property" style="color:#f92672">"test"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token property" style="color:#f92672">"build"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"tsc"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"author"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">""</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"license"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"ISC"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"description"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">""</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token property" style="color:#f92672">"release"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token property" style="color:#f92672">"branches"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"main"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token property" style="color:#f92672">"name"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"next"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token property" style="color:#f92672">"prerelease"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token property" style="color:#f92672">"plugins"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"@semantic-release/commit-analyzer"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"@semantic-release/release-notes-generator"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"@semantic-release/npm"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"@semantic-release/github"</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>Make sure name in <code>package.json</code> has your organisation name like mine <code>@devforth/</code> and you have access to publish packages to npmjs.com.</p>
<p>Also install <code>semantic-release</code>:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> i </span><span class="token parameter variable" style="color:#f8f8f2">-D</span><span class="token plain"> semantic-release</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="connecting-to-ci">Connecting to CI<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#connecting-to-ci" class="hash-link" aria-label="Direct link to Connecting to CI" title="Direct link to Connecting to CI" translate="no">​</a></h2>
<p>We will use Woodpecker CI for this example. Woodpecker is a free and open-source CI/CD tool that you can install to your own server / VPS and will not need to pay for build minutes, and will only for server. No limits on pipelines, users, repositories, etc. If you want to try it, we have <a href="https://devforth.io/blog/step-by-step-guide-to-modern-secure-ci-setup/" target="_blank" rel="noopener noreferrer" class="">Woodpecker installation guide</a></p>
<p>Create a file <code>.woodpecker.yml</code> in <code>deploy</code> directory:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/.woodpecker.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">clone</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">git</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> woodpeckerci/plugin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">git</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">settings</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">partial</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#ae81ff">false</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token key atrule">depth</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">5</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">steps</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">release</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> node</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token number" style="color:#ae81ff">22</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">when</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">event</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> push</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">commands</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> npm clean</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">install</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> npm run build</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> npm audit signatures</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> npx semantic</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">release</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">secrets</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> GITHUB_TOKEN</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> NPM_TOKEN</span><br></span></code></pre></div></div>
<p>Go to Woodpecker, authorize via GitHub, click <code>Add repository</code>, find your repository and add it.</p>
<p>Disable <code>Project settings</code> -&gt; <code>Allow Pull Requests</code> because we do not want to trigger builds on PRs.
Enable <code>Project settings</code> -&gt; <code>Trusted</code>
Enable <code>Project Visibility</code> -&gt; <code>Internal</code> unless you want to make it public.</p>
<blockquote>
<p>We strictly recommend to use <code>Internal</code> visibility for your projects, because if you use <code>Public</code> visibility, your build logs will be public and if accidentally you will print some secret to console, it will be public (generally it happens when you debug something and print environment variables).</p>
</blockquote>
<p><img decoding="async" loading="lazy" alt="Woodpecker project settings" src="https://adminforth.dev/assets/images/image-4-10736f83a40640d8963b0dd68fb3e759.png" width="2443" height="1799" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="generating-github-acces-token">Generating GitHub acces token<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#generating-github-acces-token" class="hash-link" aria-label="Direct link to Generating GitHub acces token" title="Direct link to Generating GitHub acces token" translate="no">​</a></h3>
<p>If your repo is in GitHub organisation, you need first enable access to personal access tokens for your organisation (if not yet done):</p>
<ol>
<li class="">In the upper-right corner of GitHub, select your profile photo, then click <code>Your organizations</code>.</li>
<li class="">Next to the organization, click <code>Settings</code>.</li>
<li class="">In the left sidebar, under  Personal access tokens, click <code>Settings</code>.</li>
<li class="">Select <code>Allow access via fine-grained personal access tokens</code></li>
<li class="">We recommend setting <code>Require administrator approval</code></li>
<li class="">"Allow access via personal access tokens (classic)"</li>
</ol>
<p>Now go to your profile, click on <code>Settings</code> -&gt; <code>Developer settings</code> -&gt; <code>Personal access tokens</code> -&gt; <code>Generate new token</code></p>
<p>For permissions,</p>
<ul>
<li class="">Select <code>Contents</code>: <code>Read and Write</code></li>
<li class="">Select <code>Metadata</code>: <code>Read-only</code> (if not yet selected)</li>
</ul>
<p>In Woodpecker go to <code>Settings</code>, <code>Secrets</code>, <code>Add Secret</code>, put name name: <code>GITHUB_TOKEN</code> and paste your token:</p>
<p><img decoding="async" loading="lazy" alt="Woodpecker Secrets" src="https://adminforth.dev/assets/images/image-64910662d2872a85b76892870c2ab222.png" width="3447" height="1796" class="img_ev3q"></p>
<p>In <code>Available at following events</code> select <code>Push</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="generating-npm-token">Generating NPM token<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#generating-npm-token" class="hash-link" aria-label="Direct link to Generating NPM token" title="Direct link to Generating NPM token" translate="no">​</a></h3>
<p>Go to your npmjs.com account, click on <code>Profile Avatar</code> -&gt; <code>Access Tokens</code> -&gt; <code>Generate New Token</code> -&gt; <code>New Granular Access Token</code>.</p>
<p>Packages and scopes Permissions: Read and Write.</p>
<p>In similar way to GitHub token, add it to Woodpecker as secret with name <code>NPM_TOKEN</code></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="testing">Testing<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#testing" class="hash-link" aria-label="Direct link to Testing" title="Direct link to Testing" translate="no">​</a></h2>
<p>For now we did not yet push anything to GitHub and did not publish anything to npm.</p>
<p>Lets do it now.</p>
<p>Just push your first commit as:</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">feat: initial commit</span><br></span></code></pre></div></div>
<p>This will trigger semantic-release to do first release <code>v1.0.0</code>.</p>
<p>Now change something is index.ts and push it as fix</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">fix: fix greet </span><span class="token keyword" style="color:#66d9ef">function</span><br></span></code></pre></div></div>
<p>This will trigger semantic-release to do release <code>v1.0.1</code>.</p>
<p>Now change something in <code>index.ts</code> and push it as feat</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">feat: </span><span class="token function" style="color:#e6db74">add</span><span class="token plain"> new </span><span class="token keyword" style="color:#66d9ef">function</span><br></span></code></pre></div></div>
<p>This will trigger semantic-release to do release <code>v1.1.0</code> because we added new feature, not just fixed something.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-distribution-channel">Next distribution channel<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#next-distribution-channel" class="hash-link" aria-label="Direct link to Next distribution channel" title="Direct link to Next distribution channel" translate="no">​</a></h3>
<p>Now we will show how to release to <code>next</code> channel.</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">git</span><span class="token plain"> checkout </span><span class="token parameter variable" style="color:#f8f8f2">-b</span><span class="token plain"> next</span><br></span></code></pre></div></div>
<p>Change something and push it as fix</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">fix: fix greet </span><span class="token keyword" style="color:#66d9ef">function</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">in</span><span class="token plain"> next</span><br></span></code></pre></div></div>
<p>Commit it and push:</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">git</span><span class="token plain"> push --set-upstream origin next</span><br></span></code></pre></div></div>
<p>This will trigger semantic-release to do release <code>v1.1.1-next.1</code>. Please not that it bumped patch version because we are in <code>next</code> channel.</p>
<p>Now lets add feature to next</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">feat: add new feature in next</span><br></span></code></pre></div></div>
<p>It will trigger release <code>v1.2.0-next.1</code> because we added new feature and minor version was bumped. Please not that next number started from 1 again.</p>
<p>Noe lets merge <code>next</code> to <code>main</code> and push it:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">git checkout main</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">git merge next</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">git push</span><br></span></code></pre></div></div>
<p>This will trigger release <code>v1.2.0</code> because we merged <code>next</code> to <code>main</code> and it was a feature release.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="slack-notifications-about-releases">Slack notifications about releases<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#slack-notifications-about-releases" class="hash-link" aria-label="Direct link to Slack notifications about releases" title="Direct link to Slack notifications about releases" translate="no">​</a></h2>
<p>So now we have automatic releases with release notes on GitHub.
For our internal team we use Slack and we want to get notifications about releases there.</p>
<div class="language-sh codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-sh codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> i </span><span class="token parameter variable" style="color:#f8f8f2">-D</span><span class="token plain"> semantic-release-slack-bot</span><br></span></code></pre></div></div>
<p>Into "release" section of <code>package.json</code> add slack plugin:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain"> </span><span class="token property" style="color:#f92672">"plugins"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"@semantic-release/commit-analyzer"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"@semantic-release/release-notes-generator"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"@semantic-release/npm"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token string" style="color:#a6e22e">"@semantic-release/github"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token string" style="color:#a6e22e">"semantic-release-slack-bot"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token property" style="color:#f92672">"notifyOnSuccess"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token property" style="color:#f92672">"notifyOnFail"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token property" style="color:#f92672">"slackIcon"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">":package:"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token property" style="color:#f92672">"markdownReleaseNotes"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><br></span></code></pre></div></div>
<p>Also create channel in Slack, click on channel name, go to <code>Integrations</code> -&gt; <code>Add an App</code> -&gt; <code>Incoming Webhooks</code> -&gt; <code>Add to Slack</code> -&gt; "Add Incoming Webhook to Workspace" -&gt; "Add to Slack" -&gt; "Copy Webhook URL"</p>
<p>Add it to Woodpecker as secret <code>SLACK_WEBHOOK</code> environment variable.</p>
<p>Also add this secterd to <code>.woodpecker.yml</code>:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/.woodpecker.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">secrets</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> GITHUB_TOKEN</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> NPM_TOKEN</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> SLACK_WEBHOOK</span><br></span></code></pre></div></div>
<p>This will send notifications to Slack channel about succesfull releases when <code>npm run build</code> is done without errors:</p>
<p><img decoding="async" loading="lazy" alt="Slack notifications about releases" src="https://adminforth.dev/assets/images/image-5-ec5a52db880a586bef36f2987d319596.png" width="1645" height="711" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="should-i-maintain-changelogmd-anymore">Should I maintain CHANGELOG.md anymore?<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#should-i-maintain-changelogmd-anymore" class="hash-link" aria-label="Direct link to Should I maintain CHANGELOG.md anymore?" title="Direct link to Should I maintain CHANGELOG.md anymore?" translate="no">​</a></h2>
<p><code>semantic-release</code> has a plugin for generating not only GitHub release notes, but also CHANGELOG.md.</p>
<p>Since previusly we used CHANGELOG.md I thought it would be good to have it in project. But once I entered <a href="https://github.com/semantic-release/changelog" target="_blank" rel="noopener noreferrer" class="">plugin page</a> I got a notice which <a href="https://semantic-release.gitbook.io/semantic-release/support/faq#should-release-notes-be-committed-to-a-changelog.md-in-my-repository-during-a-release" target="_blank" rel="noopener noreferrer" class="">explained complexity</a> added for this approach.</p>
<p>So we ended with a simple <a href="https://github.com/devforth/adminforth/blob/main/CHANGELOG.md" target="_blank" rel="noopener noreferrer" class="">link to GitHub releases</a></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="is-it-all-that-good">Is it all that good?<a href="https://adminforth.dev/blog/why-manual-release-notes-and-versions-are-a-chaos-and-how-to-fix-it/#is-it-all-that-good" class="hash-link" aria-label="Direct link to Is it all that good?" title="Direct link to Is it all that good?" translate="no">​</a></h2>
<p>Well, there are no perfect approaches in the world.</p>
<p>Of course <code>semantic-release</code> has some cons.</p>
<p>First of all, while you can write a commit messages without any prefix and they will not be included in release, you still have to follow strict commit message format when you are releasing feature or fix. And you don't have to forget to use this format. Instead of making manual forming of release notes which is done by one person, now every developer in team has to follow the same format and has to write clear commit messages.</p>
<p>And there are couple of bad things with last points:</p>
<ul>
<li class="">It is not so easy to modify commit message once it is pushed to GitHub, so commit writing becomes one of the most critical parts of development process where you have to be very careful.</li>
<li class="">Commit message should be understood not only by developers of framework, but also by users of framework. And some developers might think that these are two absolutely different dementions of understanding: first one is for developers, second one is for users. But in fact, at AdminForth, we think that set of user-friendly commit messages is a very small subset of set of developer-friendly commit messages. So if you write user-friendly commit messages, there is no chance that developers will not understand them.</li>
</ul>
<p>You are still fine with merging incoming PRs and ignore their commit messages becuse GitHub allows to edit commit message before merging ( using <code>Squash and merge</code> option )</p>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="Git" term="Git"/>
        <category label="Versioning" term="Versioning"/>
        <category label="NPM" term="NPM"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Backup database to AWS Glacier]]></title>
        <id>https://adminforth.dev/blog/backup-database-to-aws-glacier/</id>
        <link href="https://adminforth.dev/blog/backup-database-to-aws-glacier/"/>
        <updated>2024-12-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Every reliable system requires a backup strategy.]]></summary>
        <content type="html"><![CDATA[<p>Every reliable system requires a backup strategy.</p>
<p>If you have no own backup infrastructure, here can suggest a small docker container that will help you to backup your database to AWS Glacier.</p>
<p>As a base guide we will use a previous blog post about <a class="" href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/">deploying adminforth infrastructure</a>.</p>
<p>First we need to allocate a new bucket in AWS S3 with modifying terraform configuration:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket" "backup_bucket" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = "${local.app_name}-backups"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_lifecycle_configuration" "backup_bucket" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.backup_bucket.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  rule {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    id     = "glacier-immediate"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    status = "Enabled"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    transition {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      days          = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      storage_class = "GLACIER"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_server_side_encryption_configuration" "backup_bucket" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.backup_bucket.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  rule {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    apply_server_side_encryption_by_default {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sse_algorithm = "AES256"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_user" "backup_user" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name = "${local.app_name}-backup-user"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_access_key" "backup_user_key" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  user = aws_iam_user.backup_user.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_iam_user_policy" "backup_user_policy" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name = "${local.app_name}-backup-policy"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  user = aws_iam_user.backup_user.name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  policy = jsonencode({</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Version = "2012-10-17"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Statement = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        Effect = "Allow"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        Action = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          "s3:PutObject",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          "s3:GetObject",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          "s3:DeleteObject",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          "s3:ListBucket"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        Resource = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          aws_s3_bucket.backup_bucket.arn,</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          "${aws_s3_bucket.backup_bucket.arn}/*"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  })</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<p>Also add a section to main.tf to output the new bucket name and credentials:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "docker system prune -f",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # "docker buildx prune -f --filter 'type!=exec.cachemount'",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "cd /home/ubuntu/app/deploy",</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      "echo 'AWS_BACKUP_ACCESS_KEY=${aws_iam_access_key.backup_user_key.id}' &gt;&gt; .env.live",</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      "echo 'AWS_BACKUP_SECRET_KEY=${aws_iam_access_key.backup_user_key.secret}' &gt;&gt; .env.live",</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      "echo 'AWS_BACKUP_BUCKET=${aws_s3_bucket.backup_bucket.id}' &gt;&gt; .env.live",</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">      "echo 'AWS_BACKUP_REGION=${local.aws_region}' &gt;&gt; .env.live",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "docker compose -p app -f compose.yml --env-file ./.env.live up --build -d --quiet-pull"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span></code></pre></div></div>
<p>Add new service into compose file:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/compose.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">database_glacierizer</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> devforth/docker</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">database</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">glacierizer</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">v1.7</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">environment</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> PROJECT_NAME=MYAPP</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token comment" style="color:#8292a2;font-style:italic"># do backup every day at 00:00</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> CRON=0 0 * * *  </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DATABASE_TYPE=PostgreSQL</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DATABASE_HOST=db</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DATABASE_NAME=adminforth</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DATABASE_USER=admin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> DATABASE_PASSWORD=$</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">VAULT_POSTGRES_PASSWORD</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> GLACIER_EXPIRE_AFTER=90</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> GLACIER_STORAGE_CLASS=flexible</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> GLACIER_BUCKET_NAME=$</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">AWS_BACKUP_BUCKET</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> AWS_DEFAULT_REGION=$</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">AWS_BACKUP_REGION</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> AWS_ACCESS_KEY_ID=$</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">AWS_BACKUP_ACCESS_KEY</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> AWS_SECRET_ACCESS_KEY=$</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">AWS_BACKUP_SECRET_KEY</span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pricing">Pricing<a href="https://adminforth.dev/blog/backup-database-to-aws-glacier/#pricing" class="hash-link" aria-label="Direct link to Pricing" title="Direct link to Pricing" translate="no">​</a></h2>
<p>Just to give you row idea about pricing, here is a small calculation for case when you doing backup once per day (like in config)</p>
<ul>
<li class="">Compressed database backup has size of 50 MB always and not growing. (Compression is already done by glacierizer)</li>
<li class="">Cost of glacier every month will be ~$0.80 after first 3 month, and will stay same every next month.</li>
<li class="">On first, second and third month cost will increase slowly from $0.00 to $0.80 per month.</li>
</ul>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="AWS" term="AWS"/>
        <category label="Terraform" term="Terraform"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Deploy AdminForth to EC2 with terraform on CI]]></title>
        <id>https://adminforth.dev/blog/compose-ec2-deployment-github-actions/</id>
        <link href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/"/>
        <updated>2024-11-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Here is more advanced snippet to deploy AdminForth to Terraform.]]></summary>
        <content type="html"><![CDATA[<p>Here is more advanced snippet to deploy AdminForth to Terraform.</p>
<p>Here Terraform state will be stored in the cloud, so you can run this deployment from any machine including stateless CI/CD.</p>
<p>We will use GitHub Actions as CI/CD, but you can use any other CI/CD, for example self-hosted free WoodpeckerCI.</p>
<p>Assume you have your AdminForth project in <code>myadmin</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1---dockerfile-and-dockerignore">Step 1 - Dockerfile and .dockerignore<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-1---dockerfile-and-dockerignore" class="hash-link" aria-label="Direct link to Step 1 - Dockerfile and .dockerignore" title="Direct link to Step 1 - Dockerfile and .dockerignore" translate="no">​</a></h2>
<p>This guide assumes you have created your AdminForth application with latest version of <code>adminforth create-app</code> command.
This command already creates a <code>Dockerfile</code> and <code>.dockerignore</code> for you, so you can use them as is.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2---composeyml">Step 2 - compose.yml<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-2---composeyml" class="hash-link" aria-label="Direct link to Step 2 - compose.yml" title="Direct link to Step 2 - compose.yml" translate="no">​</a></h2>
<p>Create folder <code>deploy</code> and create file <code>compose.yml</code> inside:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/compose.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">services</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">traefik</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik:v2.5"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--api.insecure=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--providers.docker=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--entrypoints.web.address=:80"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"80:80"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"/var/run/docker.sock:/var/run/docker.sock:ro"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">myadmin</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">build</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> ../myadmin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">restart</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> always</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">env_file</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> ./myadmin/.env.secrets.prod</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> myadmin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">db</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">/code/db</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">labels</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.enable=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.routers.myadmin.rule=PathPrefix(`/`)"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.services.myadmin.loadbalancer.server.port=3500"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.routers.myadmin.priority=2"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  myadmin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">db</span><span class="token punctuation" style="color:#f8f8f2">:</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3---create-a-ssh-keypair">Step 3 - create a SSH keypair<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-3---create-a-ssh-keypair" class="hash-link" aria-label="Direct link to Step 3 - create a SSH keypair" title="Direct link to Step 3 - create a SSH keypair" translate="no">​</a></h2>
<p>Make sure you are in <code>deploy</code> folder, run next command here:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">mkdir</span><span class="token plain"> .keys </span><span class="token operator" style="color:#66d9ef">&amp;&amp;</span><span class="token plain"> ssh-keygen </span><span class="token parameter variable" style="color:#f8f8f2">-f</span><span class="token plain"> .keys/id_rsa </span><span class="token parameter variable" style="color:#f8f8f2">-N</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">""</span><br></span></code></pre></div></div>
<p>Now it should create <code>deploy/.keys/id_rsa</code> and <code>deploy/.keys/id_rsa.pub</code> files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4---gitignore-file">Step 4 - .gitignore file<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-4---gitignore-file" class="hash-link" aria-label="Direct link to Step 4 - .gitignore file" title="Direct link to Step 4 - .gitignore file" translate="no">​</a></h2>
<p>Create <code>deploy/.gitignore</code> file with next content:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">.terraform/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.keys/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfstate</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfstate.*</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tfvars</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">tfplan</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.env.secrets.prod</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5---main-terraform-file-maintf">Step 5 - Main terraform file main.tf<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-5---main-terraform-file-maintf" class="hash-link" aria-label="Direct link to Step 5 - Main terraform file main.tf" title="Direct link to Step 5 - Main terraform file main.tf" translate="no">​</a></h2>
<p>First of all install Terraform as described here <a href="https://developer.hashicorp.com/terraform/install#linux" target="_blank" rel="noopener noreferrer" class="">terraform installation</a>.</p>
<p>Create file <code>main.tf</code> in <code>deploy</code> folder:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">locals {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app_name = "&lt;your_app_name&gt;" # replace with your app name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  aws_region = "eu-central-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">provider "aws" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  region = local.aws_region</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  profile = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_ami" "ubuntu_linux" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  most_recent = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  owners      = ["amazon"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "name"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_vpc" "default" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  default = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_eip" "eip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> domain = "vpc"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_eip_association" "eip_assoc" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> instance_id   = aws_instance.app_instance.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> allocation_id = aws_eip.eip.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_subnet" "default_subnet" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "vpc-id"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = [data.aws_vpc.default.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "default-for-az"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["true"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "availability-zone"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["${local.aws_region}a"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_security_group" "instance_sg" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name   = "${local.app_name}-instance-sg"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id = data.aws_vpc.default.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow HTTP"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # SSH</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow SSH"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  egress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow all outbound traffic"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_key_pair" "app_deployer" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name   = "terraform-deploy_${local.app_name}-key"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_instance" "app_instance" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ami                    = data.aws_ami.ubuntu_linux.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  instance_type          = "t3a.small"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_id              = data.aws_subnet.default_subnet.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_security_group_ids = [aws_security_group.instance_sg.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name               = aws_key_pair.app_deployer.key_name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # prevent accidental termination of ec2 instance and data loss</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # if you will need to recreate the instance still (not sure why it can be?), you will need to remove this block manually by next command:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # &gt; terraform taint aws_instance.app_instance</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  lifecycle {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    prevent_destroy = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ignore_changes = [ami]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  root_block_device {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_size = 20 // Size in GB for root partition</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_type = "gp2"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Even if the instance is terminated, the volume will not be deleted, delete it manually if needed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    delete_on_termination = false</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  user_data = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    #!/bin/bash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get update</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get install ca-certificates curl</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo install -m 0755 -d /etc/apt/keyrings</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo chmod a+r /etc/apt/keyrings/docker.asc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Add the repository to Apt sources:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      $(. /etc/os-release &amp;&amp; echo "$VERSION_CODENAME") stable" | \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get update</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl start docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl enable docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    usermod -a -G docker ubuntu</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo "done" &gt; /home/ubuntu/user_data_done</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Name = "${local.app_name}-instance"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "wait_for_user_data" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'Waiting for EC2 software install to finish...'",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "while [ ! -f /home/ubuntu/user_data_done ]; do echo '...'; sleep 2; done",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'EC2 software install finished.'"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("./.keys/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_instance.app_instance]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "sync_files_and_run" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Use rsync to exclude node_modules, .git, db</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # heredoc syntax</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # remove files that where deleted on the source</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    #  -o StrictHostKeyChecking=no</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    rsync -t -av -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --delete \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude 'node_modules' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.git' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.terraform' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude 'terraform*' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude 'tfplan' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.keys' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.vscode' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.env' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude 'db' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ../ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Run docker compose after files have been copied</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # please note that prune might destroy build cache and make build slower, however it releases disk space</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "docker system prune -f",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # "docker buildx prune -f --filter 'type!=exec.cachemount'",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "cd /home/ubuntu/app/deploy",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "COMPOSE_BAKE=true docker compose --progress=plain -p app -f compose.yml up --build -d"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("./.keys/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Ensure the resource is triggered every time based on timestamp or file hash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    always_run = timestamp()</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [null_resource.wait_for_user_data, aws_eip_association.eip_assoc]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">output "instance_public_ip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  value = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">######### This scetion is for tf state storage ##############</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"># S3 bucket for storing Terraform state</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = "${local.app_name}-terraform-state"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  rule {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    status = "Enabled"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    id = "Keep only the latest version of the state file"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      prefix = ""</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    noncurrent_version_expiration {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      noncurrent_days = 30</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_versioning" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  versioning_configuration {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    status = "Enabled"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  bucket = aws_s3_bucket.terraform_state.bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  rule {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    apply_server_side_encryption_by_default {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sse_algorithm     = "AES256"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Replace <code>&lt;your_app_name&gt;</code> with your app name (no spaces, only underscores or letters)</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-51---configure-aws-profile">Step 5.1 - Configure AWS Profile<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-51---configure-aws-profile" class="hash-link" aria-label="Direct link to Step 5.1 - Configure AWS Profile" title="Direct link to Step 5.1 - Configure AWS Profile" translate="no">​</a></h3>
<p>Open or create file ~/.aws/credentials and add (if not already there):</p>
<div class="language-ini codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ini codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">[myaws]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_access_key_id = &lt;your_access_key&gt;</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_secret_access_key = &lt;your_secret_key&gt;</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-52---run-deployment">Step 5.2 - Run deployment<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-52---run-deployment" class="hash-link" aria-label="Direct link to Step 5.2 - Run deployment" title="Direct link to Step 5.2 - Run deployment" translate="no">​</a></h3>
<p>To run the deployment first time, you need to run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init</span><br></span></code></pre></div></div>
<p>Now run deployement:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6---migrate-state-to-the-cloud">Step 6 - Migrate state to the cloud<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-6---migrate-state-to-the-cloud" class="hash-link" aria-label="Direct link to Step 6 - Migrate state to the cloud" title="Direct link to Step 6 - Migrate state to the cloud" translate="no">​</a></h2>
<p>First deployment had to create S3 bucket for storing Terraform state. Now we need to migrate the state to the cloud.</p>
<p>Add to the end of <code>main.tf</code>:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"># Configure the backend to use the S3 bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> backend "s3" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   bucket         = "&lt;your_app_name&gt;-terraform-state"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   key            = "state.tfstate"  # Define a specific path for the state file</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   region         = "eu-central-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   profile        = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">   use_lockfile   = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span></code></pre></div></div>
<blockquote>
<p>👆 Replace <code>&lt;your_app_name&gt;</code> with your app name (no spaces, only underscores or letters).
Unfortunately we can't use variables, HashiCorp thinks it is too dangerous 😥</p>
</blockquote>
<p>Now run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init -migrate-state</span><br></span></code></pre></div></div>
<p>Now run test deployment:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>
<p>Now you can delete local <code>terraform.tfstate</code> file and <code>terraform.tfstate.backup</code> file as they are in the cloud now.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-7---cicd---github-actions">Step 7 - CI/CD - Github Actions<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-7---cicd---github-actions" class="hash-link" aria-label="Direct link to Step 7 - CI/CD - Github Actions" title="Direct link to Step 7 - CI/CD - Github Actions" translate="no">​</a></h2>
<p>Create file <code>.github/workflows/deploy.yml</code>:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.github/workflows/deploy.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Deploy </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">run-name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.actor </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> builds app 🚀</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">on</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">push</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">jobs</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">Explore-GitHub-Actions</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">runs-on</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> ubuntu</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">latest</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">steps</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🎉 The job was automatically triggered by a $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.event_name </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> event."</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🐧 This job is now running on a $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> runner.os </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> server"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🔎 The name of your branch is $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.ref </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Check out repository code</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">uses</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> actions/checkout@v4</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Set up Terraform</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">uses</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> hashicorp/setup</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">terraform@v2</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">with</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">terraform_version</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> 1.10.1 </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "💡 The $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> github.repository </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> repository has been cloned to the runner."</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Prepare env</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          echo "ADMINFORTH_SECRET=$VAULT_ADMINFORTH_SECRET" &gt; deploy/.env.secrets.prod</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_ADMINFORTH_SECRET</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_ADMINFORTH_SECRET </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">name</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> Start building</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">env</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_AWS_ACCESS_KEY_ID</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_AWS_ACCESS_KEY_ID </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_AWS_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_AWS_SECRET_ACCESS_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_SSH_PRIVATE_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_SSH_PRIVATE_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token key atrule">VAULT_SSH_PUBLIC_KEY</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> secrets.VAULT_SSH_PUBLIC_KEY </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">|</span><span class="token scalar string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token scalar string" style="color:#a6e22e">          /bin/sh -x deploy/deploy.sh</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token key atrule">run</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> echo "🍏 This job's status is $</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> job.status </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain">."</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-71---create-deploy-script">Step 7.1 - Create deploy script<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-71---create-deploy-script" class="hash-link" aria-label="Direct link to Step 7.1 - Create deploy script" title="Direct link to Step 7.1 - Create deploy script" translate="no">​</a></h3>
<p>Now create file <code>deploy/deploy.sh</code>:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">deploy/deploy.sh</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token comment" style="color:#8292a2;font-style:italic"># cd to dir of script</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token builtin class-name" style="color:#e6db74">cd</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"</span><span class="token string variable" style="color:#f8f8f2">$(</span><span class="token string variable function" style="color:#e6db74">dirname</span><span class="token string variable" style="color:#f8f8f2"> </span><span class="token string variable string" style="color:#a6e22e">"</span><span class="token string variable string variable" style="color:#f8f8f2">$0</span><span class="token string variable string" style="color:#a6e22e">"</span><span class="token string variable" style="color:#f8f8f2">)</span><span class="token string" style="color:#a6e22e">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">mkdir</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-p</span><span class="token plain"> ~/.aws ./.keys</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">cat</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">&lt;&lt;</span><span class="token string" style="color:#a6e22e">EOF</span><span class="token string bash punctuation" style="color:#f8f8f2"> </span><span class="token string bash punctuation operator" style="color:#66d9ef">&gt;</span><span class="token string bash punctuation" style="color:#f8f8f2"> ~/.aws/credentials</span><span class="token string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token string" style="color:#a6e22e">[myaws]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token string" style="color:#a6e22e">aws_access_key_id=</span><span class="token string variable" style="color:#f8f8f2">$VAULT_AWS_ACCESS_KEY_ID</span><span class="token string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token string" style="color:#a6e22e">aws_secret_access_key=</span><span class="token string variable" style="color:#f8f8f2">$VAULT_AWS_SECRET_ACCESS_KEY</span><span class="token string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token string" style="color:#a6e22e">EOF</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">cat</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">&lt;&lt;</span><span class="token string" style="color:#a6e22e">EOF</span><span class="token string bash punctuation" style="color:#f8f8f2"> </span><span class="token string bash punctuation operator" style="color:#66d9ef">&gt;</span><span class="token string bash punctuation" style="color:#f8f8f2"> ./.keys/id_rsa</span><span class="token string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token string" style="color:#a6e22e"></span><span class="token string variable" style="color:#f8f8f2">$VAULT_SSH_PRIVATE_KEY</span><span class="token string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token string" style="color:#a6e22e">EOF</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">cat</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">&lt;&lt;</span><span class="token string" style="color:#a6e22e">EOF</span><span class="token string bash punctuation" style="color:#f8f8f2"> </span><span class="token string bash punctuation operator" style="color:#66d9ef">&gt;</span><span class="token string bash punctuation" style="color:#f8f8f2"> ./.keys/id_rsa.pub</span><span class="token string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token string" style="color:#a6e22e"></span><span class="token string variable" style="color:#f8f8f2">$VAULT_SSH_PUBLIC_KEY</span><span class="token string" style="color:#a6e22e"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token string" style="color:#a6e22e">EOF</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">chmod</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">600</span><span class="token plain"> ./.keys/id_rsa*</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token comment" style="color:#8292a2;font-style:italic"># force Terraform to reinitialize the backend without migrating the state.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init </span><span class="token parameter variable" style="color:#f8f8f2">-reconfigure</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform plan </span><span class="token parameter variable" style="color:#f8f8f2">-out</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">tfplan</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply tfplan</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-72---add-secrets-to-github">Step 7.2 - Add secrets to GitHub<a href="https://adminforth.dev/blog/compose-ec2-deployment-github-actions/#step-72---add-secrets-to-github" class="hash-link" aria-label="Direct link to Step 7.2 - Add secrets to GitHub" title="Direct link to Step 7.2 - Add secrets to GitHub" translate="no">​</a></h3>
<p>Go to your GitHub repository, then <code>Settings</code> -&gt; <code>Secrets</code> -&gt; <code>New repository secret</code> and add:</p>
<ul>
<li class=""><code>VAULT_AWS_ACCESS_KEY_ID</code> - your AWS access key</li>
<li class=""><code>VAULT_AWS_SECRET_ACCESS_KEY</code> - your AWS secret key</li>
<li class=""><code>VAULT_SSH_PRIVATE_KEY</code> - make <code>cat ~/.ssh/id_rsa</code> and paste to GitHub secrets</li>
<li class=""><code>VAULT_SSH_PUBLIC_KEY</code> - make <code>cat ~/.ssh/id_rsa.pub</code> and paste to GitHub secrets</li>
<li class=""><code>VAULT_ADMINFORTH_SECRET</code> - your AdminForth secret - random string, for example <code>openssl rand -base64 32 | tr -d '\n'</code></li>
</ul>
<p>Now you can push your changes to GitHub and see how it will be deployed automatically.</p>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="AWS" term="AWS"/>
        <category label="Terraform" term="Terraform"/>
        <category label="GitHub Actions" term="GitHub Actions"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Deploy AdminForth to EC2 with terraform (without CI)]]></title>
        <id>https://adminforth.dev/blog/compose-ec2-deployment/</id>
        <link href="https://adminforth.dev/blog/compose-ec2-deployment/"/>
        <updated>2024-10-31T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Here is a row snippet to deploy AdminForth to Terraform.]]></summary>
        <content type="html"><![CDATA[<p>Here is a row snippet to deploy AdminForth to Terraform.</p>
<p>Assume you have your AdminForth project in <code>myadmin</code> created with <code>adminforth create-app</code> command. This command already creates a <code>Dockerfile</code> and <code>.dockerignore</code> for you, so you can use them as is.</p>
<p>Create file <code>compose.yml</code>:</p>
<div class="language-yml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">compose.yml</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">services</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">traefik</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">image</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik:v2.5"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">command</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--api.insecure=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--providers.docker=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"--entrypoints.web.address=:80"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">ports</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"80:80"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"/var/run/docker.sock:/var/run/docker.sock:ro"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">myadmin</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">build</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> ./myadmin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">restart</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> always</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">env_file</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> ./myadmin/.env</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> myadmin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">db</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain">/code/db</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token key atrule">labels</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.enable=true"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.routers.myadmin.rule=PathPrefix(`/`)"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.services.myadmin.loadbalancer.server.port=3500"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"traefik.http.routers.myadmin.priority=2"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token key atrule">volumes</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  myadmin</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">db</span><span class="token punctuation" style="color:#f8f8f2">:</span><br></span></code></pre></div></div>
<p>Create file <code>main.tf</code>:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">provider "aws" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  region = "eu-central-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  profile = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_ami" "ubuntu_linux" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  most_recent = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  owners      = ["amazon"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "name"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_vpc" "default" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  default = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_eip" "eip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> vpc = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_eip_association" "eip_assoc" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> instance_id   = aws_instance.myadmin_instance.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"> allocation_id = aws_eip.eip.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_subnet" "default_subnet" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "vpc-id"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = [data.aws_vpc.default.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "default-for-az"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["true"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "availability-zone"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["eu-central-1a"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_security_group" "instance_sg" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name   = "myadmin-instance-sg"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id = data.aws_vpc.default.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow HTTP"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # SSH</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow SSH"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  egress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow all outbound traffic"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_key_pair" "myadmin_deploy_key" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name   = "terraform-myadmin_deploy_key-key"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  public_key = file("~/.ssh/id_rsa.pub") # Path to your public SSH key</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_instance" "myadmin_instance" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ami                    = data.aws_ami.ubuntu_linux.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  instance_type          = "t3a.small"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_id              = data.aws_subnet.default_subnet.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_security_group_ids = [aws_security_group.instance_sg.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name               = aws_key_pair.myadmin_deploy_key.key_name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # prevent accidental termination of ec2 instance and data loss</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # if you will need to recreate the instance still (not sure why it can be?), you will need to remove this block manually by next command:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # &gt; terraform taint aws_instance.app_instance</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  lifecycle {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    prevent_destroy = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ignore_changes = [ami]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  root_block_device {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_size = 20 // Size in GB for root partition</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    volume_type = "gp2"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Even if the instance is terminated, the volume will not be deleted, delete it manually if needed</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    delete_on_termination = false</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  user_data = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    #!/bin/bash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get update</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get install ca-certificates curl</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo install -m 0755 -d /etc/apt/keyrings</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo chmod a+r /etc/apt/keyrings/docker.asc</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # Add the repository to Apt sources:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      $(. /etc/os-release &amp;&amp; echo "$VERSION_CODENAME") stable" | \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get update</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl start docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl enable docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    usermod -a -G docker ubuntu</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo "done" &gt; /home/ubuntu/user_data_done</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Name = "myadmin-instance"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "wait_for_user_data" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'Waiting for EC2 software install to finish...'",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "while [ ! -f /home/ubuntu/user_data_done ]; do echo '...'; sleep 2; done",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'EC2 software install finished.'"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("./.keys/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_instance.app_instance]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "sync_files_and_run" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Use rsync to exclude node_modules, .git, db</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    # heredoc syntax</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    rsync -t -av \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --delete \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude 'node_modules' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.git' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.terraform' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude 'terraform*' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude 'tfplan' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.keys' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.vscode' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude '.env' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      --exclude 'db' \</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      ./ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Run docker compose after files have been copied</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      # -a would destroy cache</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "docker system prune -f",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "cd /home/ubuntu/app/",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "COMPOSE_BAKE=true docker compose --progress=plain -p app -f compose.yml up --build -d"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ubuntu"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("~/.ssh/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # Ensure the resource is triggered every time based on timestamp or file hash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    always_run = timestamp()</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [null_resource.wait_for_user_data, aws_eip_association.eip_assoc]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">output "instance_public_ip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  value = aws_eip_association.eip_assoc.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span></code></pre></div></div>
<p>To run the deployment first time, you need to run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init</span><br></span></code></pre></div></div>
<p>Then with any change in code:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="AWS" term="AWS"/>
        <category label="Terraform" term="Terraform"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Build AI-Assisted blog with AdminForth and Nuxt in 20 minutes]]></title>
        <id>https://adminforth.dev/blog/ai-blog/</id>
        <link href="https://adminforth.dev/blog/ai-blog/"/>
        <updated>2024-10-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Many developers today are using copilots to write code faster and relax their minds from a routine tasks.]]></summary>
        <content type="html"><![CDATA[<p>Many developers today are using copilots to write code faster and relax their minds from a routine tasks.</p>
<p>But what about writing plain text? For example blogs and micro-blogs: sometimes you want to share your progress but you are lazy for typing. Then you can give a try to AI-assisted blogging. Our Open-Source AdminForth framework has couple of new AI-capable plugins to write text and generate images.</p>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/nuxtBlog-d00baa7e1b41d87869b09d475045fa7b.gif" width="1999" height="1499" class="img_ev3q"></p>
<p>For AI plugins are backed by OpenAI API, but their architecture allows to be easily extended for other AI providers once OpenAI competitors will reach the same or better level of quality.</p>
<p>Here we will suggest you simple as 1-2-3 steps to build and host a blog with AI assistant which will help you to write posts.</p>
<p>Our tech stack will include:</p>
<ul>
<li class=""><a href="https://nuxt.com/" target="_blank" rel="noopener noreferrer" class="">Nuxt.js</a> - SEO-friendly page rendering framework</li>
<li class=""><a href="https://adminforth.dev/" target="_blank" rel="noopener noreferrer" class="">AdminForth</a> - Admin panel framework for creating posts</li>
<li class=""><a href="https://adminforth.dev/docs/tutorial/Plugins/RichEditor/" target="_blank" rel="noopener noreferrer" class="">AdminForth RichEditor plugin</a> - WYSIWYG editor with AI assistant in Copilot style</li>
<li class="">Node and typescript</li>
<li class="">Prisma for migrations</li>
<li class="">SQLite for database, though you can easily switch it to Postgres or MongoDB</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="prerequirements">Prerequirements<a href="https://adminforth.dev/blog/ai-blog/#prerequirements" class="hash-link" aria-label="Direct link to Prerequirements" title="Direct link to Prerequirements" translate="no">​</a></h2>
<p>We will use Node v20, if you not have it installed, we recommend <a href="https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script" target="_blank" rel="noopener noreferrer" class="">NVM</a></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">nvm </span><span class="token function" style="color:#e6db74">install</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">20</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">nvm </span><span class="token builtin class-name" style="color:#e6db74">alias</span><span class="token plain"> default </span><span class="token number" style="color:#ae81ff">20</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">nvm use </span><span class="token number" style="color:#ae81ff">20</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-create-a-new-adminforth-project">Step 1: Create a new AdminForth project<a href="https://adminforth.dev/blog/ai-blog/#step-1-create-a-new-adminforth-project" class="hash-link" aria-label="Direct link to Step 1: Create a new AdminForth project" title="Direct link to Step 1: Create a new AdminForth project" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">npx adminforth create-app --app-name ai-blog</span><br></span></code></pre></div></div>
<p>Add modules:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token builtin class-name" style="color:#e6db74">cd</span><span class="token plain"> ai-blog</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> i @adminforth/upload @adminforth/rich-editor @adminforth/text-complete @adminforth/chat-gpt slugify http-proxy @adminforth/image-generation-adapter-openai @adminforth/completion-adapter-openai-responses</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-prepare-environment">Step 2: Prepare environment<a href="https://adminforth.dev/blog/ai-blog/#step-2-prepare-environment" class="hash-link" aria-label="Direct link to Step 2: Prepare environment" title="Direct link to Step 2: Prepare environment" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="openai">OpenAI<a href="https://adminforth.dev/blog/ai-blog/#openai" class="hash-link" aria-label="Direct link to OpenAI" title="Direct link to OpenAI" translate="no">​</a></h3>
<p>To allocate OpenAI API key, go to <a href="https://platform.openai.com/" target="_blank" rel="noopener noreferrer" class="">https://platform.openai.com/</a>, open Dashboard -&gt; API keys -&gt; Create new secret key.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="s3">S3<a href="https://adminforth.dev/blog/ai-blog/#s3" class="hash-link" aria-label="Direct link to S3" title="Direct link to S3" translate="no">​</a></h3>
<ol>
<li class="">Go to <a href="https://aws.amazon.com/" target="_blank" rel="noopener noreferrer" class="">https://aws.amazon.com</a> and login.</li>
<li class="">Go to Services -&gt; S3 and create a bucket. Put in bucket name e.g. <code>my-ai-blog-bucket</code>.
First of all go to your bucket settings, Permissions, scroll down to Block public access (bucket settings for this bucket) and uncheck all checkboxes.
Go to bucket settings, Permissions, Object ownership and select "ACLs Enabled" and "Bucket owner preferred" radio buttons.</li>
<li class="">Go to bucket settings, Permissions, scroll down to Cross-origin resource sharing (CORS) and put in the following configuration:</li>
</ol>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token property" style="color:#f92672">"AllowedHeaders"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token string" style="color:#a6e22e">"*"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token property" style="color:#f92672">"AllowedMethods"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token string" style="color:#a6e22e">"HEAD"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token string" style="color:#a6e22e">"PUT"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token property" style="color:#f92672">"AllowedOrigins"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">            </span><span class="token string" style="color:#a6e22e">"http://localhost:3500"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token property" style="color:#f92672">"ExposeHeaders"</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">]</span><br></span></code></pre></div></div>
<blockquote>
<p>☝️ In AllowedOrigins add all your domains. For example if you will serve blog and admin on <code>https://blog.example.com/</code> you should add
<code>"https://blog.example.com"</code> to AllowedOrigins:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">     </span><span class="token string" style="color:#a6e22e">"https://blog.example.com"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">     </span><span class="token string" style="color:#a6e22e">"http://localhost:3500"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">]</span><br></span></code></pre></div></div>
<p>Every character matters, so don't forget to add <code>http://</code> or <code>https://</code> and don't add slashes at the end of the domain.</p>
</blockquote>
<ol start="4">
<li class="">Go to Services -&gt; IAM and create a new user. Put in user name e.g. <code>my-ai-blog-bucket</code>.</li>
<li class="">Attach existing policies directly -&gt; <code>AmazonS3FullAccess</code>. Go to your user -&gt; <code>Add permissions</code> -&gt; <code>Attach policies directly</code> -&gt; <code>AmazonS3FullAccess</code></li>
<li class="">Go to Security credentials and create a new access key. Save <code>Access key ID</code> and <code>Secret access key</code>.</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="edit-env-file-in-project-directory">Edit .env file in project directory<a href="https://adminforth.dev/blog/ai-blog/#edit-env-file-in-project-directory" class="hash-link" aria-label="Direct link to Edit .env file in project directory" title="Direct link to Edit .env file in project directory" translate="no">​</a></h3>
<p>Create <code>.env</code> file with the following content:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.env</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token assign-left variable" style="color:#f8f8f2">OPENAI_API_KEY</span><span class="token operator" style="color:#66d9ef">=</span><span class="token punctuation" style="color:#f8f8f2">..</span><span class="token plain">.</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token assign-left variable" style="color:#f8f8f2">AWS_ACCESS_KEY_ID</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">your_access_key_id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token assign-left variable" style="color:#f8f8f2">AWS_SECRET_ACCESS_KEY</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">your_secret_access_key</span><br></span></code></pre></div></div>
<p>Edit <code>.env.local</code> file and add:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.env.local</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token assign-left variable" style="color:#f8f8f2">AWS_S3_BUCKET</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">my-ai-blog-bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token assign-left variable" style="color:#f8f8f2">AWS_S3_REGION</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">us-east-1</span><br></span></code></pre></div></div>
<p>In same way edit <code>.env.prod</code> file and add:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.env.prod</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token assign-left variable" style="color:#f8f8f2">AWS_S3_BUCKET</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">my-ai-blog-bucket</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token assign-left variable" style="color:#f8f8f2">AWS_S3_REGION</span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain">us-east-1</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-add-prisma-models">Step 3: Add prisma models<a href="https://adminforth.dev/blog/ai-blog/#step-3-add-prisma-models" class="hash-link" aria-label="Direct link to Step 3: Add prisma models" title="Direct link to Step 3: Add prisma models" translate="no">​</a></h2>
<p>Open <code>./schema.prisma</code> and put next content there:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./schema.prisma</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">generator client </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provider = "prisma</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">client</span><span class="token punctuation" style="color:#f8f8f2">-</span><span class="token plain">js"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">datasource db </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provider = "sqlite"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  url      = env("PRISMA_DATABASE_URL")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">model adminuser </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  id            String     @id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  email         String     @unique</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  password_hash String</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  role          String</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  created_at    DateTime</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  avatar       String</span><span class="token punctuation" style="color:#f8f8f2">?</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  public_name  String</span><span class="token punctuation" style="color:#f8f8f2">?</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  posts        Post</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">model Post </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  id          String     @id</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  createdAt   DateTime </span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  title       String</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  slug        String</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  picture     String</span><span class="token punctuation" style="color:#f8f8f2">?</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  content     String</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  published   Boolean  </span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">author      adminuser?    @relation(fields</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">authorId</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token key atrule">references</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">id</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain">)</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  authorId    String</span><span class="token punctuation" style="color:#f8f8f2">?</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  contentImages ContentImage</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">model ContentImage </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  id         String     @id</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  createdAt  DateTime </span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  img        String</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  postId     String</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  resourceId String</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token key atrule">post       Post      @relation(fields</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">postId</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token key atrule">references</span><span class="token punctuation" style="color:#f8f8f2">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">id</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain">)</span><br></span><span class="token-line code-block-diff-add-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>Create a migration:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> run makemigration -- </span><span class="token parameter variable" style="color:#f8f8f2">--name</span><span class="token plain"> add-posts </span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> run migrate:local</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4-setting-up-adminforth">Step 4: Setting up AdminForth<a href="https://adminforth.dev/blog/ai-blog/#step-4-setting-up-adminforth" class="hash-link" aria-label="Direct link to Step 4: Setting up AdminForth" title="Direct link to Step 4: Setting up AdminForth" translate="no">​</a></h2>
<p>Open <code>index.ts</code> file in root directory and update it with the following content:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./index.ts</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> express </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'express'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> AdminForth</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> Filters</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> Sorts</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> logger </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminforth'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> userResource </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'./resources/adminuser.js'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> postResource </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'./resources/posts.js'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> contentImageResource </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'./resources/content-image.js'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> httpProxy </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'http-proxy'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">declare</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">var</span><span class="token plain"> process </span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  env</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token constant" style="color:#e6db74">DATABASE_URL</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token constant" style="color:#e6db74">NODE_ENV</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token constant" style="color:#e6db74">AWS_S3_BUCKET</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token constant" style="color:#e6db74">AWS_S3_REGION</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  argv</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> admin </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">AdminForth</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  baseUrl</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'/admin'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  auth</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    usersResourceId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminuser'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">  </span><span class="token comment" style="color:#8292a2;font-style:italic">// resource to get user during login</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    usernameField</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'email'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain">  </span><span class="token comment" style="color:#8292a2;font-style:italic">// field where username is stored, should exist in resource</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    passwordHashField</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'password_hash'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  customization</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    brandName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'My Admin'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    datesFormat</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'D MMM'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    timeFormat</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'HH:mm'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    emptyFieldPlaceholder</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'-'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    styles</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      colors</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        light</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token comment" style="color:#8292a2;font-style:italic">// color for links, icons etc.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          primary</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'rgb(47 37 227)'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token comment" style="color:#8292a2;font-style:italic">// color for sidebar and text</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          sidebar</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">main</span><span class="token operator" style="color:#66d9ef">:</span><span class="token string" style="color:#a6e22e">'#EFF5F7'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> text</span><span class="token operator" style="color:#66d9ef">:</span><span class="token string" style="color:#a6e22e">'#333'</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  dataSources</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    id</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'maindb'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    url</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain">  process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">DATABASE_URL</span><span class="token operator" style="color:#66d9ef">?.</span><span class="token function" style="color:#e6db74">replace</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'file:'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'sqlite://'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  resources</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    userResource</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    postResource</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    contentImageResource</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  menu</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      homepage</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Posts'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      icon</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'flowbite:home-solid'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      resourceId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'post'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'gap'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'divider'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'heading'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'SYSTEM'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Users'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      icon</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'flowbite:user-solid'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      resourceId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminuser'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token keyword" style="color:#66d9ef">import</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">meta</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">url </span><span class="token operator" style="color:#66d9ef">===</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">file://</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">process</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">argv</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">[</span><span class="token template-string interpolation number" style="color:#ae81ff">1</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">]</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token comment" style="color:#8292a2;font-style:italic">// if script is executed directly e.g. node index.ts or npm start</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> app </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">express</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">use</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">express</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">json</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> port </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">3500</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">bundleNow</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> hotReload</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">NODE_ENV</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">===</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'development'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">then</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    logger</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">info</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'Bundling AdminForth SPA done.'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token comment" style="color:#8292a2;font-style:italic">// api to server recent posts</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">get</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'/api/posts'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> res</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> offset </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">parseInt</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">query</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">offset </span><span class="token keyword" style="color:#66d9ef">as</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">||</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">0</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> limit </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">parseInt</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">query</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">limit </span><span class="token keyword" style="color:#66d9ef">as</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">||</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">100</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> slug </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> req</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">query</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">slug </span><span class="token keyword" style="color:#66d9ef">as</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">|</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">null</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> posts </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'post'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">list</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">EQ</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'published'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">...</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">slug </span><span class="token operator" style="color:#66d9ef">?</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">LIKE</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'slug'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> slug</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      limit</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      offset</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      Sorts</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">DESC</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'createdAt'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> authorIds </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token operator" style="color:#66d9ef">...</span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">Set</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">posts</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">map</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">p</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> p</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">authorId</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> authors </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'adminuser'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">list</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">IN</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'id'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> authorIds</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">reduce</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">acc</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> a</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">acc</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">a</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> a</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> acc</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    posts</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">forEach</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">p</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> author </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> authors</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">p</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">authorId</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      p</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">author </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        publicName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> author</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">publicName</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        avatar</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">https://</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">process</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">env</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation constant" style="color:#e6db74">AWS_S3_BUCKET</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">.s3.</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">process</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">env</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation constant" style="color:#e6db74">AWS_S3_REGION</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">.amazonaws.com/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">author</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">avatar</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      p</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">picture </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">https://</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">process</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">env</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation constant" style="color:#e6db74">AWS_S3_BUCKET</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">.s3.</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">process</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">env</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation constant" style="color:#e6db74">AWS_S3_REGION</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">.amazonaws.com/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">p</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">picture</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    res</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">json</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">posts</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token comment" style="color:#8292a2;font-style:italic">// here we proxy all non-/admin requests to nuxt instance http://localhost:3000</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token comment" style="color:#8292a2;font-style:italic">// this is done for demo purposes, in production you should do this using high-performance reverse proxy like traefik or nginx</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  app</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">use</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> res</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> next</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token operator" style="color:#66d9ef">!</span><span class="token plain">req</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">url</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">startsWith</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'/admin'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">const</span><span class="token plain"> proxy </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> httpProxy</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">createProxyServer</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      proxy</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">on</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'error'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">function</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">err</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> req</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> res</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        res</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">send</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">No response from Nuxt at http://localhost:3000, did you start it? </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">err</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      proxy</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">web</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">req</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> res</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> target</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'http://localhost:3000'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">else</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function" style="color:#e6db74">next</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token comment" style="color:#8292a2;font-style:italic">// serve after you added all api</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">express</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">serve</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">app</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">discoverDatabases</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">then</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token operator" style="color:#66d9ef">!</span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'adminuser'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">get</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">Filters</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">EQ</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'email'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminforth@adminforth.dev'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">resource</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'adminuser'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">create</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        email</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminforth@adminforth.dev'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        role</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'superadmin'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        password_hash</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> AdminForth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">Utils</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">generatePasswordHash</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token string" style="color:#a6e22e">'adminforth'</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  admin</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">express</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">listen</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">port</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    logger</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">info</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">\n⚡ AdminForth is available at http://localhost:</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">port</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/admin\n</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5-edit-resources">Step 5: Edit resources<a href="https://adminforth.dev/blog/ai-blog/#step-5-edit-resources" class="hash-link" aria-label="Direct link to Step 5: Edit resources" title="Direct link to Step 5: Edit resources" translate="no">​</a></h2>
<p>Open <code>./resources/adminuser.ts</code> file with following content:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./resources/adminuser.ts</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> AdminForth</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> AdminForthDataTypes </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminforth'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> randomUUID </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'crypto'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> UploadPlugin </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'@adminforth/upload'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">default</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  dataSource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'maindb'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  table</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminuser'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Users'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token function-variable function" style="color:#e6db74">recordLabel</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">r</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">👤 </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">email</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  columns</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'id'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      primaryKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">fillOnCreate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">randomUUID</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'email'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      required</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      isUnique</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      enforceLowerCase</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      validation</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        AdminForth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">Utils</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">EMAIL_VALIDATOR</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminForthDataTypes</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">STRING</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'role'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token keyword" style="color:#66d9ef">enum</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> value</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'superadmin'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Super Admin'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> value</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'user'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'User'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'created_at'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminForthDataTypes</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">DATETIME</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">fillOnCreate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">Date</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">toISOString</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'password'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      virtual</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      required</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      editingNote</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Leave empty to keep password unchanged'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      minLength</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">8</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminForthDataTypes</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">STRING</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        show</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        list</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        filter</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      masked</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      validation</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token comment" style="color:#8292a2;font-style:italic">// request to have at least 1 digit, 1 upper case, 1 lower case</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        AdminForth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">Utils</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">PASSWORD_VALIDATORS</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">UP_LOW_NUM</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'password_hash'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> backendOnly</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> all</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'public_name'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminForthDataTypes</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">STRING</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'avatar'</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  hooks</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">beforeSave</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> record</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminUser</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> resource </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">passwordHash </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> AdminForth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">Utils</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">generatePasswordHash</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">password</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> ok</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">beforeSave</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> record</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminUser</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> resource </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">password</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">passwordHash </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">await</span><span class="token plain"> AdminForth</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">Utils</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">generatePasswordHash</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">password</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> ok</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  plugins</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">UploadPlugin</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      pathColumnName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'avatar'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3Bucket</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_S3_BUCKET</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3Region</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_S3_REGION</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      allowedFileExtensions</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token string" style="color:#a6e22e">'jpg'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'jpeg'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'png'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'gif'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'webm'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token string" style="color:#a6e22e">'webp'</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      maxFileSize</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">1024</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">*</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">1024</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">*</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">20</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token comment" style="color:#8292a2;font-style:italic">// 20MB</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3AccessKeyId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_ACCESS_KEY_ID</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3SecretAccessKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3ACL</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'public-read'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token comment" style="color:#8292a2;font-style:italic">// ACL which will be set to uploaded file</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">s3Path</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> originalFilename</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> originalExtension </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">originalFilename</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> originalExtension</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">user-avatars/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation keyword" style="color:#66d9ef">new</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation class-name" style="color:#e6db74">Date</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation function" style="color:#e6db74">getFullYear</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation function" style="color:#e6db74">randomUUID</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">originalFilename</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">.</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">originalExtension</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      generation</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        provider</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'openai'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        countToGenerate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">2</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        openAiOptions</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          model</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'gpt-4o'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          apiKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">OPENAI_API_KEY</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>Create <code>posts.ts</code> file in res directory with following content:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./resources/post.ts</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> AdminUser</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> AdminForthDataTypes </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminforth'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> randomUUID </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'crypto'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> UploadPlugin </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'@adminforth/upload'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> RichEditorPlugin </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'@adminforth/rich-editor'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> ChatGptPlugin </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'@adminforth/chat-gpt'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> slugify </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'slugify'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> CompletionAdapterOpenAIResponses </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"@adminforth/completion-adapter-openai-responses"</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> ImageGenerationAdapterOpenAI </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'@adminforth/image-generation-adapter-openai'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">default</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  table</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'post'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  dataSource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'maindb'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Posts'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token function-variable function" style="color:#e6db74">recordLabel</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">r</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">📝 </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">title</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  columns</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'id'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      primaryKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">fillOnCreate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">randomUUID</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        list</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'title'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      required</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> all</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      maxLength</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">255</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      minLength</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">3</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminForthDataTypes</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">STRING</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'picture'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> all</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'slug'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        list</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'content'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> list</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminForthDataTypes</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">TEXT</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      components</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        show</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"@/renderers/RichText.vue"</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'createdAt'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">fillOnCreate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">Date</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">toISOString</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'published'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      required</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'authorId'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      foreignResource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        resourceId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminuser'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        list</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">fillOnCreate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> adminUser </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> adminUser</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminUser </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> adminUser</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">dbUser</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">id</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  hooks</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">beforeSave</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> record</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminUser </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> record</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminUser</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminUser </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">slug </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">slugify</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">title</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> lower</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> ok</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">beforeSave</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">async</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> record</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminUser </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> record</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> adminUser</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminUser </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">if</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">title</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">slug </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">slugify</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">record</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">title</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> lower</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token keyword" style="color:#66d9ef">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> ok</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  plugins</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">UploadPlugin</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      pathColumnName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'picture'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3Bucket</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_S3_BUCKET</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3Region</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_S3_REGION</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      allowedFileExtensions</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token string" style="color:#a6e22e">'jpg'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'jpeg'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'png'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'gif'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'webm'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token string" style="color:#a6e22e">'webp'</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      maxFileSize</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">1024</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">*</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">1024</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">*</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">20</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token comment" style="color:#8292a2;font-style:italic">// 20MB</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3AccessKeyId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_ACCESS_KEY_ID</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3SecretAccessKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3ACL</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'public-read'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token comment" style="color:#8292a2;font-style:italic">// ACL which will be set to uploaded file</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">s3Path</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> originalFilename</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> originalExtension </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">originalFilename</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> originalExtension</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">post-previews/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation keyword" style="color:#66d9ef">new</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation class-name" style="color:#e6db74">Date</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation function" style="color:#e6db74">getFullYear</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation function" style="color:#e6db74">randomUUID</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">originalFilename</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">.</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">originalExtension</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      generation</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        countToGenerate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">2</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        adapter</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">ImageGenerationAdapterOpenAI</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          openAiApiKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">OPENAI_API_KEY</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">as</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          model</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'gpt-image-1'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        fieldsForContext</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token string" style="color:#a6e22e">'title'</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        outputSize</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'1536x1024'</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">RichEditorPlugin</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      htmlFieldName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'content'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      completion</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        adapter</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">CompletionAdapterOpenAIResponses</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          openAiApiKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">OPENAI_API_KEY</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">as</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          model</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'gpt-4o'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          expert</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">            temperature</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">0.7</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        expert</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          debounceTime</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">250</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      attachments</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        attachmentResource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'contentImage'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        attachmentFieldName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'img'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        attachmentRecordIdFieldName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'postId'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        attachmentResourceIdFieldName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'resourceId'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">ChatGptPlugin</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      openAiApiKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">OPENAI_API_KEY</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      model</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'gpt-4o'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      fieldName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'title'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      expert</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        debounceTime</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">250</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>Also create <code>content-image.ts</code> file in <code>res</code> directory with following content:</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./resources/content-image.ts</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> AdminForthDataTypes </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'adminforth'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> randomUUID </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'crypto'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">import</span><span class="token plain"> UploadPlugin </span><span class="token keyword" style="color:#66d9ef">from</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'@adminforth/upload'</span><span class="token punctuation" style="color:#f8f8f2">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token keyword" style="color:#66d9ef">export</span><span class="token plain"> </span><span class="token keyword" style="color:#66d9ef">default</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  table</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'contentImage'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  dataSource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'maindb'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  label</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'Content Images'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token function-variable function" style="color:#e6db74">recordLabel</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain">r</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">any</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">🖼️ </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">r</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation">img</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  columns</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'id'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      primaryKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">fillOnCreate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">randomUUID</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'createdAt'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminForthDataTypes</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">DATETIME</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">fillOnCreate</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">Date</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token function" style="color:#e6db74">toISOString</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'img'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> AdminForthDataTypes</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">STRING</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      required</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">true</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'postId'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      foreignResource</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        resourceId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'post'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      showIn</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        edit</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        create</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token boolean" style="color:#ae81ff">false</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      name</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'resourceId'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  plugins</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token keyword" style="color:#66d9ef">new</span><span class="token plain"> </span><span class="token class-name" style="color:#e6db74">UploadPlugin</span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      pathColumnName</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'img'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3Bucket</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_S3_BUCKET</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3Region</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_S3_REGION</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      allowedFileExtensions</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token string" style="color:#a6e22e">'jpg'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'jpeg'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'png'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'gif'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'webm'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token string" style="color:#a6e22e">'webp'</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      maxFileSize</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">1024</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">*</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">1024</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">*</span><span class="token plain"> </span><span class="token number" style="color:#ae81ff">20</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token comment" style="color:#8292a2;font-style:italic">// 20MB</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3AccessKeyId</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_ACCESS_KEY_ID</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3SecretAccessKey</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> process</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token plain">env</span><span class="token punctuation" style="color:#f8f8f2">.</span><span class="token constant" style="color:#e6db74">AWS_SECRET_ACCESS_KEY</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      s3ACL</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">'public-read'</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> </span><span class="token comment" style="color:#8292a2;font-style:italic">// ACL which will be set to uploaded file</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token function-variable function" style="color:#e6db74">s3Path</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain"> originalFilename</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> originalExtension </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">{</span><span class="token plain">originalFilename</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"> originalExtension</span><span class="token operator" style="color:#66d9ef">:</span><span class="token plain"> </span><span class="token builtin" style="color:#e6db74">string</span><span class="token plain"> </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">=&gt;</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token template-string string" style="color:#a6e22e">post-content/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation keyword" style="color:#66d9ef">new</span><span class="token template-string interpolation"> </span><span class="token template-string interpolation class-name" style="color:#e6db74">Date</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">.</span><span class="token template-string interpolation function" style="color:#e6db74">getFullYear</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation function" style="color:#e6db74">randomUUID</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">(</span><span class="token template-string interpolation punctuation" style="color:#f8f8f2">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">/</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">originalFilename</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string string" style="color:#a6e22e">.</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">${</span><span class="token template-string interpolation">originalExtension</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:#f8f8f2">}</span><span class="token template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token punctuation" style="color:#f8f8f2">}</span><span class="token punctuation" style="color:#f8f8f2">)</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token punctuation" style="color:#f8f8f2">,</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">}</span><br></span></code></pre></div></div>
<p>Now you can start your admin panel:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> run dev</span><br></span></code></pre></div></div>
<p>Open <code>http://localhost:3500/admin</code> in your browser and login with <code>adminforth@adminforth.dev</code> and <code>adminforth</code> credentials.
Set up your avatar (you can generate it with AI) and public name in user settings.</p>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/aiblogpost-0dd2eb35b7b6ebca0822a2cc79711c44.png" width="3670" height="2588" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6-create-nuxt-project">Step 6: Create Nuxt project<a href="https://adminforth.dev/blog/ai-blog/#step-6-create-nuxt-project" class="hash-link" aria-label="Direct link to Step 6: Create Nuxt project" title="Direct link to Step 6: Create Nuxt project" translate="no">​</a></h2>
<p>Now let's initialize our seo-facing frontend.
In the root directory of your admin app (<code>ai-blog</code>) and create a new folder <code>seo</code> and run:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">npx nuxi@latest init seo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token builtin class-name" style="color:#e6db74">cd</span><span class="token plain"> seo</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">install</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-D</span><span class="token plain"> sass-embedded</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> run dev</span><br></span></code></pre></div></div>
<p>Edit <code>app.vue</code>:</p>
<div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./seo/app.vue</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">template</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">id</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">app</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">NuxtPage</span><span class="token tag" style="color:#f92672"> </span><span class="token tag punctuation" style="color:#f8f8f2">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">template</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">style</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">lang</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">scss</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">$</span><span class="token style language-css property" style="color:#f92672">grColor1</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#74E1FF</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">$</span><span class="token style language-css property" style="color:#f92672">grColor2</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#8580B4</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">$</span><span class="token style language-css property" style="color:#f92672">grColor3</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#5E53C3</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">$</span><span class="token style language-css property" style="color:#f92672">grColor4</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#4FC7E9</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">$</span><span class="token style language-css property" style="color:#f92672">grColor5</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#695BE9</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css selector id" style="color:#a6e22e;font-style:italic">#app</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">font-family</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> Avenir</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> Helvetica</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> Arial</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> sans-serif</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">-webkit-font-smoothing</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> antialiased</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">-moz-osx-font-smoothing</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> grayscale</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    // gradient with color spots</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">animation</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> gradient </span><span class="token style language-css number" style="color:#ae81ff">15</span><span class="token style language-css unit">s</span><span class="token style language-css"> ease infinite</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">min-height</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">100</span><span class="token style language-css unit">dvh</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css selector" style="color:#a6e22e;font-style:italic">body</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">margin</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">padding</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">max-height</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">100</span><span class="token style language-css unit">dvh</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">overflow</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> overlay</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">background-image</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css function" style="color:#e6db74">radial-gradient</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  circle farthest-corner at top left</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> $grColor1 </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> </span><span class="token style language-css color function" style="color:#e6db74">rgba</span><span class="token style language-css color punctuation" style="color:#f8f8f2">(</span><span class="token style language-css color number" style="color:#ae81ff">225</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">243</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">97</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">)</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">50</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css function" style="color:#e6db74">radial-gradient</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      circle farthest-side at top right</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> $grColor2 </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> </span><span class="token style language-css color function" style="color:#e6db74">rgba</span><span class="token style language-css color punctuation" style="color:#f8f8f2">(</span><span class="token style language-css color number" style="color:#ae81ff">181</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">176</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">177</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">)</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">10</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css function" style="color:#e6db74">radial-gradient</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css">circle farthest-corner at bottom right</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> $grColor3 </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> </span><span class="token style language-css color function" style="color:#e6db74">rgba</span><span class="token style language-css color punctuation" style="color:#f8f8f2">(</span><span class="token style language-css color number" style="color:#ae81ff">204</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">104</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">119</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">)</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">33</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css function" style="color:#e6db74">radial-gradient</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">          circle farthest-corner at top right</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> $grColor4 </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> </span><span class="token style language-css color function" style="color:#e6db74">rgba</span><span class="token style language-css color punctuation" style="color:#f8f8f2">(</span><span class="token style language-css color number" style="color:#ae81ff">155</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">221</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">240</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">)</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">50</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css function" style="color:#e6db74">radial-gradient</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css">ellipse at bottom center</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> $grColor5 </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> </span><span class="token style language-css color function" style="color:#e6db74">rgba</span><span class="token style language-css color punctuation" style="color:#f8f8f2">(</span><span class="token style language-css color number" style="color:#ae81ff">254</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">43</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">)</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">80</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">background-attachment</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> fixed</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">style</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><br></span></code></pre></div></div>
<p>Add folder <code>pages</code> and create <code>index.vue</code>:</p>
<div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./seo/pages/index.vue</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">template</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">class</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">container</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">PostCard</span><span class="token tag" style="color:#f92672"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token tag" style="color:#f92672">      </span><span class="token tag attr-name" style="color:#a6e22e !important">v-for</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">post in posts</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token tag" style="color:#f92672">      </span><span class="token tag attr-name" style="color:#a6e22e !important">:key</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">post.id</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token tag" style="color:#f92672">      </span><span class="token tag attr-name" style="color:#a6e22e !important">:post</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">post</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token tag" style="color:#f92672">    </span><span class="token tag punctuation" style="color:#f8f8f2">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">class</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">no-posts</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">v-if</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">!posts.length</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      No posts added yet</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">a</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">href</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">/admin</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain">Add a first one in admin</span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">a</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">template</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">style</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">lang</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">scss</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token style language-css selector class" style="color:#a6e22e;font-style:italic">.container</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">display</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> flex</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">justify-content</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> center</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">align-items</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> center</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">flex-wrap</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> wrap</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">flex-direction</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> column</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">gap</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">padding-top</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token style language-css selector class" style="color:#a6e22e;font-style:italic">.no-posts</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">margin-top</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">font-size</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">text-align</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> center</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">background-color</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css function" style="color:#e6db74">rgba</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css number" style="color:#ae81ff">255</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">244</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">255</span><span class="token style language-css"> </span><span class="token style language-css operator" style="color:#66d9ef">/</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.43</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">padding</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">border-radius</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">border</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1</span><span class="token style language-css unit">px</span><span class="token style language-css"> solid </span><span class="token style language-css hexcode color">#FFFFFF</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">box-shadow</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.2</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.3</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css color function" style="color:#e6db74">rgba</span><span class="token style language-css color punctuation" style="color:#f8f8f2">(</span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0.1</span><span class="token style language-css color punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">color</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#555</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css selector" style="color:#a6e22e;font-style:italic">a</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">color</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#333</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">text-decoration</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> underline</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">margin-top</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">display</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> block</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">font-size</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1.2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">style</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">script</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">lang</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">ts</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">setup</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token script language-javascript keyword module" style="color:#66d9ef">import</span><span class="token script language-javascript"> </span><span class="token script language-javascript imports maybe-class-name">PostCard</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword module" style="color:#66d9ef">from</span><span class="token script language-javascript"> </span><span class="token script language-javascript string" style="color:#a6e22e">'~/PostCard.vue'</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token script language-javascript keyword" style="color:#66d9ef">const</span><span class="token script language-javascript"> posts </span><span class="token script language-javascript operator" style="color:#66d9ef">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript function" style="color:#e6db74">ref</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">[</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">]</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token script language-javascript function" style="color:#e6db74">onMounted</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript keyword" style="color:#66d9ef">async</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript arrow operator" style="color:#66d9ef">=&gt;</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">{</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">  </span><span class="token script language-javascript keyword" style="color:#66d9ef">const</span><span class="token script language-javascript"> resp </span><span class="token script language-javascript operator" style="color:#66d9ef">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword control-flow" style="color:#66d9ef">await</span><span class="token script language-javascript"> </span><span class="token script language-javascript function" style="color:#e6db74">fetch</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token script language-javascript template-string string" style="color:#a6e22e">/api/posts</span><span class="token script language-javascript template-string template-punctuation string" style="color:#a6e22e">`</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">;</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">  posts</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">.</span><span class="token script language-javascript property-access">value</span><span class="token script language-javascript"> </span><span class="token script language-javascript operator" style="color:#66d9ef">=</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword control-flow" style="color:#66d9ef">await</span><span class="token script language-javascript"> resp</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">.</span><span class="token script language-javascript method function property-access" style="color:#e6db74">json</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">;</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token script language-javascript punctuation" style="color:#f8f8f2">}</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">script</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><br></span></code></pre></div></div>
<p>Finally, create <code>PostCard.vue</code> component:</p>
<div class="language-html codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./seo/PostCard.vue</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-html codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">template</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">class</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">post-card</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">img</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">v-if</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">props.post.picture</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">:src</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">props.post.picture</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">alt</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">post image</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><span class="token tag punctuation" style="color:#f8f8f2">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">h2</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain">{{ props.post.title }}</span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">h2</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">class</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">content</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">v-html</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">props.post.content</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">class</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">posted-at</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain">{{ formatDate(props.post.createdAt) }}</span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">class</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">author</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">img</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">:src</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">props.post.author.avatar</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">alt</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">author avatar</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag" style="color:#f92672"> </span><span class="token tag punctuation" style="color:#f8f8f2">/&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">          {{ props.post.author.publicName }}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">        </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">div</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">template</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">script</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">setup</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">lang</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">ts</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token script language-javascript keyword" style="color:#66d9ef">const</span><span class="token script language-javascript"> props </span><span class="token script language-javascript operator" style="color:#66d9ef">=</span><span class="token script language-javascript"> defineProps</span><span class="token script language-javascript operator" style="color:#66d9ef">&lt;</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">{</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">  </span><span class="token script language-javascript literal-property property" style="color:#f92672">post</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">{</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property" style="color:#f92672">title</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> string</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property" style="color:#f92672">content</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> string</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property" style="color:#f92672">createdAt</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> string </span><span class="token script language-javascript comment" style="color:#8292a2;font-style:italic">// iso date</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    picture</span><span class="token script language-javascript operator" style="color:#66d9ef">?</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> string</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property" style="color:#f92672">author</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">{</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">      </span><span class="token script language-javascript literal-property property" style="color:#f92672">publicName</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> string</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">      </span><span class="token script language-javascript literal-property property" style="color:#f92672">avatar</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> string</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">}</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">  </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">}</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token script language-javascript punctuation" style="color:#f8f8f2">}</span><span class="token script language-javascript operator" style="color:#66d9ef">&gt;</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token script language-javascript keyword" style="color:#66d9ef">function</span><span class="token script language-javascript"> </span><span class="token script language-javascript function" style="color:#e6db74">formatDate</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript parameter literal-property property" style="color:#f92672">date</span><span class="token script language-javascript parameter operator" style="color:#66d9ef">:</span><span class="token script language-javascript parameter"> string</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">{</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">  </span><span class="token script language-javascript comment" style="color:#8292a2;font-style:italic">// format to format MMM DD, YYYY using Intl.DateTimeFormat</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">  </span><span class="token script language-javascript keyword control-flow" style="color:#66d9ef">return</span><span class="token script language-javascript"> </span><span class="token script language-javascript keyword" style="color:#66d9ef">new</span><span class="token script language-javascript"> </span><span class="token script language-javascript class-name" style="color:#e6db74">Intl</span><span class="token script language-javascript class-name punctuation" style="color:#f8f8f2">.</span><span class="token script language-javascript class-name" style="color:#e6db74">DateTimeFormat</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript string" style="color:#a6e22e">'en-US'</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">,</span><span class="token script language-javascript"> </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">{</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property" style="color:#f92672">month</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript string" style="color:#a6e22e">'short'</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">,</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property" style="color:#f92672">day</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript string" style="color:#a6e22e">'2-digit'</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">,</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">    </span><span class="token script language-javascript literal-property property" style="color:#f92672">year</span><span class="token script language-javascript operator" style="color:#66d9ef">:</span><span class="token script language-javascript"> </span><span class="token script language-javascript string" style="color:#a6e22e">'numeric'</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript">  </span><span class="token script language-javascript punctuation" style="color:#f8f8f2">}</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">.</span><span class="token script language-javascript method function property-access" style="color:#e6db74">format</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript keyword" style="color:#66d9ef">new</span><span class="token script language-javascript"> </span><span class="token script language-javascript class-name" style="color:#e6db74">Date</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">(</span><span class="token script language-javascript">date</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript punctuation" style="color:#f8f8f2">)</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token script language-javascript punctuation" style="color:#f8f8f2">}</span><span class="token script language-javascript"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token script language-javascript"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">script</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;</span><span class="token tag" style="color:#f92672">style</span><span class="token tag" style="color:#f92672"> </span><span class="token tag attr-name" style="color:#a6e22e !important">lang</span><span class="token tag attr-value punctuation attr-equals" style="color:#f8f8f2">=</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag attr-value" style="color:#f92672">scss</span><span class="token tag attr-value punctuation" style="color:#f8f8f2">"</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token style language-css selector class" style="color:#a6e22e;font-style:italic">.post-card</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">background-color</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css function" style="color:#e6db74">rgba</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css number" style="color:#ae81ff">255</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">244</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">255</span><span class="token style language-css"> </span><span class="token style language-css operator" style="color:#66d9ef">/</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.43</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">padding</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">border-radius</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">border</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1</span><span class="token style language-css unit">px</span><span class="token style language-css"> solid </span><span class="token style language-css hexcode color">#FFFFFF</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">box-shadow</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.2</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.3</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css color function" style="color:#e6db74">rgba</span><span class="token style language-css color punctuation" style="color:#f8f8f2">(</span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0</span><span class="token style language-css color punctuation" style="color:#f8f8f2">,</span><span class="token style language-css color"> </span><span class="token style language-css color number" style="color:#ae81ff">0.1</span><span class="token style language-css color punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">max-width</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css function" style="color:#e6db74">calc</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css number" style="color:#ae81ff">100</span><span class="token style language-css unit">vw</span><span class="token style language-css"> </span><span class="token style language-css operator" style="color:#66d9ef">-</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">4</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">width</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">600</span><span class="token style language-css unit">px</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">color</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#333</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css property" style="color:#f92672">line-height</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1.8</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css selector combinator" style="color:#a6e22e;font-style:italic">&gt;</span><span class="token style language-css selector" style="color:#a6e22e;font-style:italic">img</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">width</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">100</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">border-radius</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">margin-bottom</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css selector" style="color:#a6e22e;font-style:italic">h2</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">margin</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">font-size</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css selector class" style="color:#a6e22e;font-style:italic">.content</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">margin-top</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css selector class" style="color:#a6e22e;font-style:italic">.posted-at</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">margin-top</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">font-size</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.8</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">color</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css hexcode color">#666</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">display</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> flex</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">justify-content</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> space-between</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">align-items</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> center</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css selector class" style="color:#a6e22e;font-style:italic">.author</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">display</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> flex</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css property" style="color:#f92672">align-items</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> center</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css selector" style="color:#a6e22e;font-style:italic">img</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">width</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">height</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">border-radius</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">50</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">margin-right</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css selector" style="color:#a6e22e;font-style:italic">div</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      // flash wire dot line effect</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">position</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> relative</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">overflow</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> hidden</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">border-radius</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">padding</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.2</span><span class="token style language-css unit">rem</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0.5</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">font-size</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">1</span><span class="token style language-css unit">rem</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">background</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css function" style="color:#e6db74">linear-gradient</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css number" style="color:#ae81ff">90</span><span class="token style language-css unit">deg</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> </span><span class="token style language-css function" style="color:#e6db74">rgb</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">21</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">255</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">,</span><span class="token style language-css"> </span><span class="token style language-css function" style="color:#e6db74">rgb</span><span class="token style language-css punctuation" style="color:#f8f8f2">(</span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">0</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">100</span><span class="token style language-css unit">%</span><span class="token style language-css punctuation" style="color:#f8f8f2">)</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">background-size</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">200</span><span class="token style language-css unit">%</span><span class="token style language-css"> auto</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">background-clip</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> text</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">-webkit-background-clip</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> text</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">color</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css color">transparent</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"> </span><span class="token style language-css comment" style="color:#8292a2;font-style:italic">/* Hide the original text color */</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css property" style="color:#f92672">animation</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> shimmer </span><span class="token style language-css number" style="color:#ae81ff">2</span><span class="token style language-css unit">s</span><span class="token style language-css"> infinite</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css atrule rule" style="color:#e6db74">@keyframes</span><span class="token style language-css atrule"> shimmer</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">        </span><span class="token style language-css selector" style="color:#a6e22e;font-style:italic">0%</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">          </span><span class="token style language-css property" style="color:#f92672">background-position</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">-200</span><span class="token style language-css unit">%</span><span class="token style language-css"> center</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">        </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">        </span><span class="token style language-css selector" style="color:#a6e22e;font-style:italic">100%</span><span class="token style language-css"> </span><span class="token style language-css punctuation" style="color:#f8f8f2">{</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">          </span><span class="token style language-css property" style="color:#f92672">background-position</span><span class="token style language-css punctuation" style="color:#f8f8f2">:</span><span class="token style language-css"> </span><span class="token style language-css number" style="color:#ae81ff">200</span><span class="token style language-css unit">%</span><span class="token style language-css"> center</span><span class="token style language-css punctuation" style="color:#f8f8f2">;</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">        </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">      </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">    </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css">  </span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token style language-css punctuation" style="color:#f8f8f2">}</span><span class="token style language-css"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token style language-css"></span><span class="token tag punctuation" style="color:#f8f8f2">&lt;/</span><span class="token tag" style="color:#f92672">style</span><span class="token tag punctuation" style="color:#f8f8f2">&gt;</span><br></span></code></pre></div></div>
<p>Now you can start your Nuxt project:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> run dev</span><br></span></code></pre></div></div>
<p>And run <code>npm start</code> if you did not run it previously:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">npm</span><span class="token plain"> start</span><br></span></code></pre></div></div>
<p>Open <code>http://localhost:3500</code> in your browser and you will see your blog with posts from admin panel:</p>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/localhost_3500_-c9ecd1678af4f1eaf02d27640104af5e.png" width="3700" height="1932" class="img_ev3q"></p>
<p>Go to <code>http://localhost:3500/admin</code> to add new posts.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-7-deploy">Step 7: Deploy<a href="https://adminforth.dev/blog/ai-blog/#step-7-deploy" class="hash-link" aria-label="Direct link to Step 7: Deploy" title="Direct link to Step 7: Deploy" translate="no">​</a></h2>
<p>We will use Docker to make it easy to deploy with many ways. We will wrap both Node.js adminforth app and Nuxt.js app into single container for simplicity using supervisor. However you can split them into two containers and deploy them separately e.g. using docker compose.</p>
<p>Please note that in this demo example we routing requests to Nuxt.js app from AdminForth app using http-proxy.
While this will work fine, it might give slower serving then if you would route traffik using dedicated reverse proxies like traefik or nginx.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="dockerize-adminforth-and-nuxt-in-single-container">Dockerize AdminForth and Nuxt in single container<a href="https://adminforth.dev/blog/ai-blog/#dockerize-adminforth-and-nuxt-in-single-container" class="hash-link" aria-label="Direct link to Dockerize AdminForth and Nuxt in single container" title="Direct link to Dockerize AdminForth and Nuxt in single container" translate="no">​</a></h3>
<p>Open <code>Dockerfile</code> in root project directory (<code>ai-blog</code>) and put in the following content:</p>
<div class="language-dockerfile codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./Dockerfile</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-dockerfile codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token instruction keyword" style="color:#66d9ef">FROM</span><span class="token instruction"> node:20-slim</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">EXPOSE</span><span class="token instruction"> 3500</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">WORKDIR</span><span class="token instruction"> /app</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">RUN</span><span class="token instruction"> apt-get update &amp;&amp; apt-get install -y supervisor</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">COPY</span><span class="token instruction"> package.json package-lock.json ./</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">RUN</span><span class="token instruction"> npm ci</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">COPY</span><span class="token instruction"> seo/package.json seo/package-lock.json seo/</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">RUN</span><span class="token instruction"> cd seo &amp;&amp; npm ci</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">COPY</span><span class="token instruction"> . .</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">RUN</span><span class="token instruction"> npx adminforth bundle</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">RUN</span><span class="token instruction"> cd seo &amp;&amp; npm run build</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">RUN</span><span class="token instruction"> cat &gt; /etc/supervisord.conf &lt;&lt;EOF</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">[supervisord]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">nodaemon=true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">[program:app]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">command=npm run prod</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">directory=/app</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">autostart=true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">autorestart=true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stdout_logfile=/dev/stdout</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stderr_logfile=/dev/stderr</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stdout_logfile_maxbytes = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stderr_logfile_maxbytes = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">[program:seo]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">command=sh -c "cd seo &amp;&amp; node .output/server/index.mjs"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">directory=/app</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">autostart=true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">autorestart=true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stdout_logfile=/dev/stdout</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stderr_logfile=/dev/stderr</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stdout_logfile_maxbytes = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stderr_logfile_maxbytes = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">[program:prisma]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">command=npm run migrate:prod</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">directory=/app</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">autostart=true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stdout_logfile=/dev/stdout</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stderr_logfile=/dev/stderr</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stdout_logfile_maxbytes = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">stderr_logfile_maxbytes = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token instruction keyword" style="color:#66d9ef">CMD</span><span class="token instruction"> [</span><span class="token instruction string" style="color:#a6e22e">"supervisord"</span><span class="token instruction">, </span><span class="token instruction string" style="color:#a6e22e">"-c"</span><span class="token instruction">, </span><span class="token instruction string" style="color:#a6e22e">"/etc/supervisord.conf"</span><span class="token instruction">]</span><br></span></code></pre></div></div>
<p>Open <code>.dockerignore</code> file in root project directory (<code>ai-blog</code>) and put in the following content:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">.dockerignore</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">.env</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">node_modules</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">seo/node_modules</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.git</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">db</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tar</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">.terraform*</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform*</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">*.tf</span><br></span></code></pre></div></div>
<p>Build and run your docker container locally:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">docker</span><span class="token plain"> build </span><span class="token parameter variable" style="color:#f8f8f2">-t</span><span class="token plain"> my-ai-blog </span><span class="token builtin class-name" style="color:#e6db74">.</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">docker</span><span class="token plain"> run </span><span class="token parameter variable" style="color:#f8f8f2">-p80:3500</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-v</span><span class="token plain"> ./prodDb:/app/db --env-file .env </span><span class="token parameter variable" style="color:#f8f8f2">-it</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">--name</span><span class="token plain"> my-ai-blog </span><span class="token parameter variable" style="color:#f8f8f2">-d</span><span class="token plain"> my-ai-blog</span><br></span></code></pre></div></div>
<p>Now you can open <code>http://localhost</code> in your browser and see your blog.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="deploy-to-ec2-with-terraform">Deploy to EC2 with terraform<a href="https://adminforth.dev/blog/ai-blog/#deploy-to-ec2-with-terraform" class="hash-link" aria-label="Direct link to Deploy to EC2 with terraform" title="Direct link to Deploy to EC2 with terraform" translate="no">​</a></h3>
<p>First of all install Terraform as described here <a href="https://developer.hashicorp.com/terraform/install#linux" target="_blank" rel="noopener noreferrer" class="">terraform installation</a>.</p>
<p>If you are on Ubuntu(WSL2 or native) you can use the following commands:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token function" style="color:#e6db74">wget</span><span class="token plain"> -O- https://apt.releases.hashicorp.com/gpg </span><span class="token operator" style="color:#66d9ef">|</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> gpg </span><span class="token parameter variable" style="color:#f8f8f2">--dearmor</span><span class="token plain"> </span><span class="token parameter variable" style="color:#f8f8f2">-o</span><span class="token plain"> /usr/share/keyrings/hashicorp-archive-keyring.gpg</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token builtin class-name" style="color:#e6db74">echo</span><span class="token plain"> </span><span class="token string" style="color:#a6e22e">"deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com </span><span class="token string variable" style="color:#f8f8f2">$(</span><span class="token string variable" style="color:#f8f8f2">lsb_release </span><span class="token string variable parameter variable" style="color:#f8f8f2">-cs</span><span class="token string variable" style="color:#f8f8f2">)</span><span class="token string" style="color:#a6e22e"> main"</span><span class="token plain"> </span><span class="token operator" style="color:#66d9ef">|</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">tee</span><span class="token plain"> /etc/apt/sources.list.d/hashicorp.list</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">apt</span><span class="token plain"> update </span><span class="token operator" style="color:#66d9ef">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">sudo</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">apt</span><span class="token plain"> </span><span class="token function" style="color:#e6db74">install</span><span class="token plain"> terraform</span><br></span></code></pre></div></div>
<p>Create special AWS credentials for deployemnts by going to <code>AWS console</code> -&gt; <code>IAM</code> -&gt; <code>Users</code> -&gt; <code>Add user</code> (e.g. my-ai-blog-user) -&gt; Attach existing policies directly -&gt; <code>AdministratorAccess</code> -&gt; Create user. Save <code>Access key ID</code> and <code>Secret access key</code> into <code>~/.aws/credentials</code> file:</p>
<p>Create or open file:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">code ~/.aws/credentials</span><br></span></code></pre></div></div>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token punctuation" style="color:#f8f8f2">..</span><span class="token plain">.</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain"></span><span class="token punctuation" style="color:#f8f8f2">[</span><span class="token plain">myaws</span><span class="token punctuation" style="color:#f8f8f2">]</span><span class="token plain"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_access_key_id </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> YOUR_ACCESS_KEY</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">aws_secret_access_key </span><span class="token operator" style="color:#66d9ef">=</span><span class="token plain"> YOUR_SECRET</span><br></span></code></pre></div></div>
<p>Create file <code>main.tf</code> in root project directory:</p>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockTitle_OeMC">./main.tf</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">provider "aws" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  region = "eu-central-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  profile = "myaws"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_ami" "amazon_linux" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  most_recent = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  owners      = ["amazon"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "name"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["amzn2-ami-hvm-*-x86_64-gp2"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_vpc" "default" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  default = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">data "aws_subnet" "default_subnet" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "vpc-id"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = [data.aws_vpc.default.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "default-for-az"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["true"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  filter {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    name   = "availability-zone"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    values = ["eu-central-1a"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_security_group" "instance_sg" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  name   = "my-aiblog-instance-sg"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_id = data.aws_vpc.default.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow HTTP"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 80</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # SSH</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ingress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow SSH"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 22</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "tcp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  egress {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    description = "Allow all outbound traffic"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    from_port   = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    to_port     = 0</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    protocol    = "-1"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    cidr_blocks = ["0.0.0.0/0"]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_key_pair" "deployer" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name   = "terraform-deployer-my-aiblog-key"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  public_key = file("~/.ssh/id_rsa.pub") # Path to your public SSH key</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "aws_instance" "docker_instance" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  ami                    = data.aws_ami.amazon_linux.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  instance_type          = "t3a.small"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  subnet_id              = data.aws_subnet.default_subnet.id</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  vpc_security_group_ids = [aws_security_group.instance_sg.id]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  key_name               = aws_key_pair.deployer.key_name</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # prevent accidental termination of ec2 instance and data loss</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # if you will need to recreate the instance still (not sure why it can be?), you will need to remove this block manually by next command:</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  # &gt; terraform taint aws_instance.app_instance</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  lifecycle {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    prevent_destroy = true</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ignore_changes = [ami]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  user_data = &lt;&lt;-EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    #!/bin/bash</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    yum update -y</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    amazon-linux-extras install docker -y</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl start docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    systemctl enable docker</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    usermod -a -G docker ec2-user</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    echo "done" &gt; /home/ec2-user/user-data-done</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  EOF</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  tags = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    Name = "my-ai-blog-instance"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "wait_for_user_data" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'Waiting for EC2 software install to finish...'",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "while [ ! -f /home/ec2-user/user-data-done ]; do sleep 2; done",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "echo 'EC2 software install finished.'"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ec2-user"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("~/.ssh/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_instance.docker_instance.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [aws_instance.docker_instance]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "build_image" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "local-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    command = "docker build -t blogapp . &amp;&amp; docker save blogapp:latest -o blogapp_image.tar"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    always_run = timestamp() # Force re-run if necessary</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">resource "null_resource" "remote_commands" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  depends_on = [null_resource.wait_for_user_data, null_resource.build_image]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  triggers = {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    always_run = timestamp()</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "file" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    source      = "${path.module}/blogapp_image.tar"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    destination = "/home/ec2-user/blogapp_image.tar"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ec2-user"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("~/.ssh/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_instance.docker_instance.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "file" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    source      = "${path.module}/.env"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    destination = "/home/ec2-user/.env"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ec2-user"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("~/.ssh/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_instance.docker_instance.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  provisioner "remote-exec" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    inline = [</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "sudo docker system prune -af",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "docker load -i /home/ec2-user/blogapp_image.tar",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "sudo docker rm -f blogapp || true",</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      "sudo docker run --env-file .env -d -p 80:3500 --name blogapp -v /home/ec2-user/db:/app/db blogapp"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    ]</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    connection {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      type        = "ssh"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      user        = "ec2-user"</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      private_key = file("~/.ssh/id_rsa")</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">      host        = aws_instance.docker_instance.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">    }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  }</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  </span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">output "instance_public_ip" {</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">  value = aws_instance.docker_instance.public_ip</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">}</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<p>Now you can deploy your app to AWS EC2:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform init</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">terraform apply -auto-approve</span><br></span></code></pre></div></div>
<blockquote>
<p>☝️ To destroy and  stop billing run <code>terraform destroy -auto-approve</code></p>
</blockquote>
<blockquote>
<p>☝️ To check logs run <code>ssh -i ~/.ssh/id_rsa ec2-user@$(terraform output instance_public_ip)</code>, then <code>sudo docker logs -n100 -f aiblog</code></p>
</blockquote>
<p>Terraform config will build the Docker image locally and then copy it to the EC2 instance. This approach saves build resources (CPU/RAM) on the EC2 instance, though it increases network traffic (the image might be around 200MB). If you prefer to build the image directly on the EC2 instance, you can slightly adjust the configuration: remove <code>null_resource.build_image</code> and modify <code>null_resource.remote_commands</code> to perform the build remotely. However, note that building the image on a <code>t3.small</code> instance may still consume significant resources and can interfere with running applications. To avoid potential downtime or performance issues, building the image locally remains the recommended approach.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="add-https-and-cdn">Add HTTPs and CDN<a href="https://adminforth.dev/blog/ai-blog/#add-https-and-cdn" class="hash-link" aria-label="Direct link to Add HTTPs and CDN" title="Direct link to Add HTTPs and CDN" translate="no">​</a></h3>
<p>For adding HTTPS and CDN you will use free Cloudflare service (though you can use paid AWS Cloudfront or any different way e.g. add Traefik and Let's Encrypt). Go to <a href="https://cloudflare.com/" target="_blank" rel="noopener noreferrer" class="">https://cloudflare.com</a> and create an account. Add your domain and follow instructions to change your domain nameservers to Cloudflare ones.</p>
<p>Go to your domain settings and add A record with your server IP address, which was shown in output of <code>terraform apply</code> command.</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">Type: A</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">Name: blog</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">Value: x.y.z.w</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">Cloudflare proxy: orange (enabled)</span><br></span></code></pre></div></div>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/image-19ae59e490ade7cd0c77f839f664ecf5.png" width="2476" height="502" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="useful-links">Useful links<a href="https://adminforth.dev/blog/ai-blog/#useful-links" class="hash-link" aria-label="Direct link to Useful links" title="Direct link to Useful links" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://github.com/devforth/adminforth-example-ai-blog" target="_blank" rel="noopener noreferrer" class="">Full source code of the project</a></li>
<li class=""><a href="https://blog-demo.adminforth.dev/admin/resource/post" target="_blank" rel="noopener noreferrer" class="">Live demo of AI BLog</a></li>
<li class=""><a href="https://adminforth.dev/docs/tutorial/gettingStarted/" target="_blank" rel="noopener noreferrer" class="">AdminForth documentation</a></li>
<li class=""><a href="https://github.com/devforth/adminforth" target="_blank" rel="noopener noreferrer" class="">AdminForth GitHub</a></li>
<li class=""><a href="https://nuxt.com/docs/getting-started/introduction" target="_blank" rel="noopener noreferrer" class="">Nuxt.js documentation</a></li>
</ul>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="Nuxt.js" term="Nuxt.js"/>
        <category label="ChatGPT" term="ChatGPT"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Chat-GPT plugin to co-write texts and strings]]></title>
        <id>https://adminforth.dev/blog/chatgpt-plugin/</id>
        <link href="https://adminforth.dev/blog/chatgpt-plugin/"/>
        <updated>2024-08-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Couple of days ago we released a plugin which allows you to co-write texts and strings with the AI.]]></summary>
        <content type="html"><![CDATA[<p>Couple of days ago we released a plugin which allows you to co-write texts and strings with the AI.</p>
<p>Today LLM is already a must tool to speed-up writing, brainstorming, or generating ideas.</p>
<p>Here is how it looks in action:</p>
<p><img decoding="async" loading="lazy" alt="alt text" src="https://adminforth.dev/assets/images/demoChatGpt-cb045146add2758d6fb571efef680e80.gif" width="1999" height="1499" class="img_ev3q"></p>
<p>To control plugin we use our open-source <a href="https://github.com/devforth/vue-suggestion-input" target="_blank" rel="noopener noreferrer" class="">vue-suggestion-input</a>.
It allows to:</p>
<ul>
<li class="">Complete suggestion with <code>Tab</code>.</li>
<li class="">Complete word with <code>Ctrl + Right</code>.</li>
<li class="">Regenerate suggestion with <code>Ctrl + Down</code>.</li>
<li class="">On mobile suggestion word is accepted with swipe right on the screen.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="want-to-try-it-out">Want to try it out?<a href="https://adminforth.dev/blog/chatgpt-plugin/#want-to-try-it-out" class="hash-link" aria-label="Direct link to Want to try it out?" title="Direct link to Want to try it out?" translate="no">​</a></h2>
<p>Go to a <a href="https://demo.adminforth.dev/resource/aparts/create" target="_blank" rel="noopener noreferrer" class="">Live Demo</a> and start creating a new apartment record. Type in the <code>title</code> and <code>description</code> field and see how the plugin works.</p>
<p>If you want to try it out on your hello-wrold admin panel, then, first follow the instructions in the <a class="" href="https://adminforth.dev/docs/tutorial/gettingStarted/">Getting Started</a> tutorial to create a new project. To install the plugin, then, follow the instructions in the <a class="" href="https://adminforth.dev/docs/tutorial/Plugins/text-complete/">Chat-GPT plugin page</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="context-matters-but-with-sane-limit">Context matters, but with sane limit!<a href="https://adminforth.dev/blog/chatgpt-plugin/#context-matters-but-with-sane-limit" class="hash-link" aria-label="Direct link to Context matters, but with sane limit!" title="Direct link to Context matters, but with sane limit!" translate="no">​</a></h2>
<p>When the prompts are called, the plugin passes to LLM not only previous text in current field to complete, but also passes values of other fields in record edited. This allows to generate more relevant completions.
For example if you have a record with fields <code>title</code> and <code>description</code>, and you are editing <code>description</code>, the plugin will pass <code>title</code> value to LLM as well.</p>
<p>But obviously longer prompts lead to higher LLM costs and longer response times. That is why we created mechanics to limit the length of prompts passed to LLM.</p>
<p>Limitation is done on 2 levels:</p>
<ul>
<li class="">plugin settings have <code>expert.promptInputLimit</code> - limits length of edited field passed to LLM. If field is longer, it will pass only last <code>promptInputLimit</code> characters.</li>
<li class="">plugin settings have <code>expert.recordContext</code> which defines limits for other fields in record. Each field can't be longer then <code>maxFieldLength</code> (default is 300). If field is longer then it is split to parts <code>splitParts</code> and they are joined with '...'. Also if there are more non-empty fields then <code>maxFields</code>, then plugin selects top longest <code>maxFields</code> fields to pass to LLM.</li>
</ul>
<p>In the end, total number of characters passed to LLM is limited by formula:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">promptInputLimit + maxFields * maxFieldLength + &lt;LLM request static part&gt;</span><br></span></code></pre></div></div>
<p>Where <code>&lt;LLM request static part&gt;</code> is a constant part of request to LLM which looks like this:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#f8f8f2;--prism-background-color:#272822"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#f8f8f2;background-color:#272822"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#f8f8f2"><span class="token plain">Continue writing for text/string field "${this.options.fieldName}" in the table "${resLabel}"\n</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">Record has values for the context: ${inputContext}\n</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">Current field value: ${currentVal}\n</span><br></span><span class="token-line" style="color:#f8f8f2"><span class="token plain">Don't talk to me. Just write text. No quotes. Don't repeat current field value, just write completion\n</span><br></span></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="model-configuration">Model configuration<a href="https://adminforth.dev/blog/chatgpt-plugin/#model-configuration" class="hash-link" aria-label="Direct link to Model configuration" title="Direct link to Model configuration" translate="no">​</a></h2>
<p>Of course you can define which model to use for completion. By default plugin uses <code>gpt-4o-mini</code> model ($0.150 / 1M input tokens, $0.600 / 1M output tokens for Aug 2024). But you can change it to any other model available in OpenAI API. More powerful replacement is <code>gpt-4o</code> model ($5.00 / 1M input tokens, $15.00 / 1M output tokens for Aug 2024).</p>
<p>Also you can define other parameters for completion like:</p>
<ul>
<li class=""><code>maxTokens</code> - most likely you don't want to waste tokens on longer completions, so default is 50 tokens.</li>
<li class=""><code>temperature</code> - model temperature, default is 0.7. You can increase it to get more creative completions (but with risk of getting nonsense). Or decrease it to get more conservative completions.</li>
<li class=""><code>debounceTime</code> - debounce time in milliseconds, default is 300. After typing each character, plugin waits for <code>debounceTime</code> milliseconds before sending request to LLM. If new character is typed during this time, then timer is reset. This is done to prevent sending too many requests to LLM.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frontend-story">Frontend story<a href="https://adminforth.dev/blog/chatgpt-plugin/#frontend-story" class="hash-link" aria-label="Direct link to Frontend story" title="Direct link to Frontend story" translate="no">​</a></h2>
<p>When we were working on plugin, we wanted to make it as user-friendly as possible.</p>
<p>Most frontend packages for completion have old-fashioned dropdowns, which are not very convenient to use.</p>
<p>We wanted to have something very similar to Copilot or Google doc. So we created our own package <a href="https://github.com/devforth/vue-suggestion-input" target="_blank" rel="noopener noreferrer" class="">vue-suggestion-input</a>. It is also MIT and open-source so you can use it in your projects as well.</p>
<p>Under the hood vue-suggestion-input uses <a href="https://quilljs.com/" target="_blank" rel="noopener noreferrer" class="">quill</a> editor. Quill is one of the WYSIWYG editors which have really good
API to work with DOM inside of editor. Basically all pieces of content in editor are represented as so called blots. And best thing - you can create your own custom blot. So we created our own blot which is responsible for rendering completion suggestion. Then you just "dance" around positioning of selection, suggestion and text modification, and thees things are easy-peasy with quill API.</p>]]></content>
        <author>
            <name>Ivan Borshchov</name>
            <uri>https://github.com/ivictbor</uri>
        </author>
        <category label="ChatGPT" term="ChatGPT"/>
        <category label="Plugin" term="Plugin"/>
    </entry>
</feed>