Deploy a Containerized Service to K3s
Deploy a Flask order service to k3s on EC2 using ArgoCD GitOps and SQS.
Introduction
30 Second Summary
When you order something online, your request passes through a chain of services that validate, queue, and route it to the right place. Modern teams build these services as small, independent containers that can be deployed and updated without touching anything else.
In this project, you will build a containerized order-processing API, deploy it to a lightweight Kubernetes cluster running on an AWS EC2 instance, and manage deployments through a GitOps pipeline powered by ArgoCD.
What You'll Build
A Python Flask microservice that accepts orders via a REST API and publishes them to an Amazon SQS queue, containerized with Docker, stored in Amazon ECR, and deployed to k3s on EC2 via ArgoCD.
By the end of this project, you'll have:
- A containerized Python Flask API pushed to Amazon ECR and running on a single-node k3s Kubernetes cluster.
- An EC2 instance IAM role granting your application SQS access with no hardcoded credentials.
- A fully automated GitOps pipeline where ArgoCD watches your GitHub repository and deploys changes to Kubernetes automatically.
- Secret Mission: Trigger an automated deployment by pushing a code change and watching ArgoCD sync it live.
Are there any prerequisites?
You need an AWS account (paid plan recommended), a GitHub account, and an SSH client installed on your machine. You should also have the AWS CLI, Docker, and Git already set up locally.
This project costs approximately $0.10-$0.20 for a few hours of work on a single t3.medium EC2 instance. Teardown instructions are included so you can delete everything when you are done.
Launch EC2 and Install k3s
Every production deployment needs a place to run. In this project, your order service will live on a Kubernetes cluster. Instead of a managed service, you will build your own single-node cluster using k3s on an EC2 instance.
Before you can deploy anything, you need infrastructure. This step sets up the foundation: an EC2 instance with the right permissions, a lightweight Kubernetes cluster, and a container registry to store your images.
In this step, get ready to:
- Create an IAM role and security group, then launch an EC2 instance.
- SSH into the instance and install k3s to get a working Kubernetes cluster.
- Create an ECR repository to store your container images.
Create the IAM role and launch the EC2 instance
Your application will need to pull images from ECR and send messages to SQS. Instead of hardcoding credentials, you will attach an IAM role to the EC2 instance so it inherits permissions automatically.
- Sign in to the AWS Management Console.
- In the top search bar, type IAM and select IAM.
- In the left navigation pane, select Roles, then select Create role.
- For Trusted entity type, choose AWS service.
- Under Use case, choose EC2, then select Next.
- On the Add permissions page, search for AmazonEC2ContainerRegistryReadOnly and check the box next to it.
- Search again for AmazonSQSFullAccess and check that box too.
- Select Next.
- For Role name, enter order-service-role.
- Select Create role.
What is an IAM role for EC2?
An IAM role lets your EC2 instance make AWS API calls (pulling images, sending messages) without storing access keys on the machine. The instance automatically receives temporary credentials through its instance profile.
Now create a security group to control which traffic can reach your instance.
- In the top search bar, type EC2 and select EC2.
- In the left navigation pane, select Security Groups (under Network & Security).
- Select Create security group.
- For Security group name, enter order-service-sg.
- For Description, enter Allow SSH, HTTP, and k3s API.
- Under Inbound rules, select Add rule three times to create three rules:
- Rule 1: Type = SSH, Port = 22, Source = 0.0.0.0/0.
- Rule 2: Type = HTTP, Port = 80, Source = 0.0.0.0/0.
- Rule 3: Type = Custom TCP, Port = 6443, Source = 0.0.0.0/0.
- Select Create security group.
Why port 6443?
Port 6443 is the Kubernetes API server port. k3s exposes the cluster API here so tools like kubectl and ArgoCD can communicate with the cluster.
Now launch the EC2 instance itself.
- In the left navigation pane, select Instances, then select Launch instances.
- For Name, enter order-service-node.
- Under Application and OS Images, search for Ubuntu and select Ubuntu Server 22.04 LTS (free tier eligible AMI).
- For Instance type, select t3.medium.
- Under Key pair (login), select Create new key pair.
- Name it order-service-key, keep the default settings (RSA, .pem), and select Create key pair.
- The .pem file downloads automatically. Save it somewhere you can find it.
- Under Network settings, select Select existing security group and choose order-service-sg.
- Expand Advanced details.
- For IAM instance profile, select order-service-role from the dropdown.
- Select Launch instance.
Why t3.medium?
k3s requires a minimum of 2 CPU cores and 2 GB RAM. ArgoCD adds roughly 1.5 GB of memory usage on top of that. The t3.medium instance provides 2 vCPUs and 4 GiB RAM, which gives enough headroom for k3s, ArgoCD, and your workload.
- Wait for the instance state to show Running and the status checks to pass (1-2 minutes).
- Select the instance and copy its Public IPv4 address from the details panel.
Don't see order-service-role in the IAM instance profile dropdown?
The console creates the instance profile automatically when you create a role through the IAM console. If the role does not appear, go back to IAM and confirm the role was created with EC2 as the trusted entity. Refresh the EC2 launch page and try again.
Help me troubleshoot this
SSH in and install k3s
With your instance running, you can connect to it and set up Kubernetes. k3s is a lightweight, CNCF-certified Kubernetes distribution that installs in seconds with a single command.
- Open your local terminal.
- Set the correct permissions on your key file by running this command:
chmod 400 [[PEMPATH="path/to/order-service-key.pem"]]
- Connect to your EC2 instance by running this command:
ssh -i [[PEMPATH="path/to/order-service-key.pem"]] ubuntu@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]
- You will see a prompt asking if you want to continue connecting. Type yes and press Enter.
You should see the Ubuntu welcome message and a prompt like ubuntu@ip-...:~$.
Getting "Permission denied" or "Connection timed out"?
If you see a permissions error, make sure you ran the chmod 400 command on your .pem file. If the connection times out, check that your security group order-service-sg has an SSH rule on port 22 with source 0.0.0.0/0.
Help me troubleshoot my SSH connection
- Install k3s on the EC2 instance by running this command:
curl -sfL https://get.k3s.io | sh -
What does this command do?
This downloads and runs the official k3s install script. It installs a full Kubernetes cluster (control plane + worker) as a single binary. It also installs kubectl automatically and starts the k3s service.
k3s bundles containerd (container runtime), CoreDNS, Flannel (networking), and a ServiceLB load balancer controller. All of these would normally be separate installs on a full Kubernetes cluster.
The install takes about 30 seconds. Once it finishes, configure kubectl to use the k3s cluster.
- Set up kubeconfig access by running these commands:
sudo mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
What does this do?
k3s stores its kubeconfig at /etc/rancher/k3s/k3s.yaml with root-only permissions. These commands copy it to the standard ~/.kube/config location and set your user as the owner, so kubectl can read it without sudo.
- Verify the cluster is running by running this command:
kubectl get nodes
You should see one node with a Ready status. It may take up to 30 seconds after install for the node to become Ready.
Why k3s instead of EKS?
Amazon EKS charges $0.10/hr just for the control plane. A single t3.medium running k3s costs $0.0418/hr and gives you a fully functional, CNCF-certified Kubernetes cluster. All the same tools (kubectl, ArgoCD, standard manifests) work identically on both.
The tradeoff is that you manage the control plane yourself. For a learning project, this is actually an advantage because you see the full picture.
Node showing NotReady?
Wait 30-60 seconds and run kubectl get nodes again. k3s needs a moment to initialize its networking components. If it stays NotReady after two minutes, check the k3s service logs with sudo journalctl -u k3s.
Help me debug my k3s node status
Create the ECR repository
You need a container registry to store the Docker image you will build in the next step. Amazon ECR is a managed registry that integrates with the IAM role you already created.
- Open a new terminal tab on your local machine (not the SSH session).
- Create the ECR repository by running this command:
aws ecr create-repository \
--repository-name order-service \
--region us-east-1
What does this command do?
This creates a private container registry named order-service in your AWS account. You will push your Docker image here, and the EC2 instance will pull from it using the AmazonEC2ContainerRegistryReadOnly policy you attached earlier.
You should see a JSON response containing the repositoryUri field. Note this URI. You will use it later when tagging and pushing your image.
Getting an access denied error?
Make sure your local AWS CLI is configured with valid credentials. Run aws sts get-caller-identity to confirm your CLI is authenticated. If you see an error about the region, add --region us-east-1 to the command or set your default region with aws configure.
Help me fix my AWS CLI credentials
Your infrastructure is live. You have a Kubernetes cluster running on EC2 and a container registry ready to store images. Next, you will build the order service application and push it to ECR.
Build and Push the Order Service Container
In the previous step, you launched an EC2 instance, installed k3s, and set up your container registry. Your Kubernetes cluster is running and waiting for a workload.
Now you need an application to deploy. In this step, you will write a Python Flask API that processes orders via SQS, package it into a Docker container image, and push that image to Amazon ECR so Kubernetes can pull it later.
In this step, get ready to:
- Create the Flask order-processing API and its dependencies file.
- Write a Dockerfile to containerize the application.
- Build, tag, and push the container image to ECR.
Create the Flask application
The order service is a lightweight REST API. It accepts order data via a POST request, publishes the order to an SQS queue for async processing, and exposes a health endpoint for Kubernetes probes.
- Create a project directory and move into it by running these commands:
mkdir ~/order-service
cd ~/order-service
- Create the application file by running:
cat > app.py << 'EOF'
import os
import json
from flask import Flask, request, jsonify
import boto3
app = Flask(__name__)
sqs = boto3.client("sqs", region_name=os.environ.get("AWS_REGION", "us-east-1"))
QUEUE_URL = os.environ["SQS_QUEUE_URL"]
@app.route("/orders", methods=["POST"])
def create_order():
order_data = request.get_json()
if not order_data:
return jsonify({"error": "No order data provided"}), 400
message_body = json.dumps(order_data)
response = sqs.send_message(QueueUrl=QUEUE_URL, MessageBody=message_body)
message_id = response["MessageId"]
return jsonify({"message": "Order received", "message_id": message_id}), 201
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "healthy", "version": "1.0"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
EOF
What does this code do?
The POST /orders endpoint reads JSON from the request body, serializes it, and sends it as a message to an SQS queue. It returns the SQS message ID to confirm the order was queued.
The GET /health endpoint returns a simple JSON response. Kubernetes uses this to check whether the pod is alive and ready to serve traffic.
Both the queue URL and AWS region come from environment variables. The app never stores credentials directly because the EC2 instance's IAM role handles authentication automatically through the metadata service.
- Create the dependencies file by running:
cat > requirements.txt << 'EOF'
flask==3.1.1
boto3==1.38.23
EOF
What are these dependencies?
Flask is the web framework that handles HTTP routing. boto3 is the AWS SDK for Python, which lets the app interact with SQS.
- Confirm both files exist by running:
ls ~/order-service
You should see app.py and requirements.txt listed in the output.
Don't see both files?
Make sure you ran the commands from within the ~/order-service directory. If the directory is missing, re-run mkdir ~/order-service first.
Help me figure out why my files weren't created in the right place.
Write the Dockerfile
A Dockerfile describes how to package your application into a container image. It specifies the base image, copies your code in, installs dependencies, and defines how to start the app.
- Create the Dockerfile by running:
cat > Dockerfile << 'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
EOF
What does this Dockerfile do?
- FROM python:3.12-slim starts from a minimal Python image to keep the container small.
- COPY requirements.txt then RUN pip install installs dependencies first. This layer is cached so rebuilds are fast when only app.py changes.
- EXPOSE 5000 documents the port Flask listens on.
- CMD ["python", "app.py"] tells Docker how to start the application when the container runs.
- Confirm the Dockerfile exists by running:
ls ~/order-service
You should see app.py, requirements.txt, and Dockerfile listed.
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here is what each file should contain at this point.
app.py:
import os
import json
from flask import Flask, request, jsonify
import boto3
app = Flask(__name__)
sqs = boto3.client("sqs", region_name=os.environ.get("AWS_REGION", "us-east-1"))
QUEUE_URL = os.environ["SQS_QUEUE_URL"]
@app.route("/orders", methods=["POST"])
def create_order():
order_data = request.get_json()
if not order_data:
return jsonify({"error": "No order data provided"}), 400
message_body = json.dumps(order_data)
response = sqs.send_message(QueueUrl=QUEUE_URL, MessageBody=message_body)
message_id = response["MessageId"]
return jsonify({"message": "Order received", "message_id": message_id}), 201
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "healthy", "version": "1.0"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
requirements.txt:
flask==3.1.1
boto3==1.38.23
Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
Install Docker, build, and push to ECR
k3s uses containerd as its container runtime, but it does not include Docker for building images. You need to install Docker separately on the EC2 instance so you can build and push your image to ECR.
Why install Docker separately?
k3s runs containers using containerd directly. Docker is only needed here as a build tool to create and push images. Once the image is in ECR, k3s pulls it using containerd without Docker involved.
- Set up Docker's apt repository by running:
# Add Docker's official GPG key
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to apt sources
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
What does this command do?
This adds Docker's official package repository to your system. It downloads Docker's GPG signing key (so apt can verify packages are authentic) and registers the repository URL. The final sudo apt update refreshes apt so it knows about Docker's packages.
- Install Docker Engine by running:
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin
- Verify Docker is installed by running:
sudo docker --version
You should see a Docker version number printed in the output.
Docker command not found?
If the install fails, make sure the GPG key was downloaded correctly. Re-run the repository setup commands above. Also confirm you are running Ubuntu 22.04 by running lsb_release -a.
Help me troubleshoot Docker installation on my Ubuntu EC2 instance.
- Navigate to your project directory and build the Docker image by running:
cd ~/order-service
sudo docker build -t order-service:v1 .
What does this command do?
The -t order-service:v1 flag names and tags the image. The . tells Docker to use the current directory (which contains your Dockerfile) as the build context.
The build takes about 30-60 seconds as it downloads the Python base image and installs the pip dependencies. You should see a success message at the end.
- Authenticate Docker to your ECR registry by running:
aws ecr get-login-password --region [[AWS_REGION="your AWS region e.g. us-east-1"]] | sudo docker login --username AWS --password-stdin [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com
What does this command do?
The aws ecr get-login-password command retrieves a temporary authentication token from AWS. That token is piped to docker login so Docker can push images to your private ECR registry. The token is valid for 12 hours.
You should see Login Succeeded in the output.
- Tag the image with your ECR repository URI by running:
sudo docker tag order-service:v1 [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com/order-service:v1
- Push the image to ECR by running:
sudo docker push [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.[[AWS_REGION="your AWS region e.g. us-east-1"]].amazonaws.com/order-service:v1
The push uploads your image layers to ECR. This may take a minute depending on your instance's network speed.
- Verify the image is in ECR by running:
aws ecr list-images --repository-name order-service --region [[AWS_REGION="your AWS region e.g. us-east-1"]]
You should see your image with the tag v1 listed in the output.
Push failed with an authorization error?
Make sure your EC2 instance has the order-service-role IAM role attached. The role needs write access to ECR for pushing images. If you only attached the AmazonEC2ContainerRegistryReadOnly policy, add AmazonEC2ContainerRegistryPowerUser to your role in the IAM console.
Also confirm your region matches between the ECR repository and the login command.
Help me fix ECR push authorization errors on my EC2 instance.
Your container image is now stored in ECR and ready to be pulled by Kubernetes. Next up, you will create an SQS queue and write the Kubernetes manifests that deploy this image to your cluster.
Set Up SQS and Create Kubernetes Manifests
Your container image is built and stored in ECR, ready to be pulled by any Kubernetes node. Now you need somewhere for your application to send orders and a set of instructions telling Kubernetes how to run your container.
In this step, you will create an SQS queue for async message processing and write the Kubernetes manifests that define your Deployment, Service, and ConfigMap. Then you will push those manifests to a dedicated GitOps repository so ArgoCD can pick them up in the next step.
In this step, get ready to:
- Create an SQS queue for order message processing.
- Write Kubernetes manifests describing your application workload.
- Push the manifests to a GitHub repository for GitOps delivery.
Create the SQS queue
Your Flask app expects an SQS queue URL as an environment variable. The queue is where order data gets published for downstream consumers to process asynchronously.
- Create the SQS queue by running this command on your EC2 instance:
aws sqs create-queue --queue-name order-queue
What does this command do?
This creates a standard SQS queue named order-queue in the same region as your EC2 instance.
The command returns a JSON response containing the QueueUrl field. This URL is what your Flask app uses to send messages to the queue.
- Copy the QueueUrl value from the output. It looks like https://sqs.us-east-1.amazonaws.com/123456789012/order-queue with your region and account ID.
- Verify the queue exists by running:
aws sqs get-queue-url --queue-name order-queue
You should see the same QueueUrl value returned. Keep this URL handy for the next substep.
Seeing an error about permissions?
Make sure your EC2 instance has the IAM role with AmazonSQSFullAccess attached. You can verify by running aws sts get-caller-identity and confirming the ARN shows your order-service-role.
If the region is wrong, set it explicitly with export AWS_DEFAULT_REGION=us-east-1 (using your region) and try again.
Why can't I create my SQS queue from the EC2 instance?
Create the Kubernetes manifests
Kubernetes uses YAML manifest files to declare the desired state of your application. You will create four manifests: a ConfigMap for environment variables, a Deployment describing your pods, a Service to expose them, and an ArgoCD Application resource for GitOps delivery.
- Create the GitOps repo directory and manifests folder by running:
mkdir -p ~/order-service-gitops/k8s
cd ~/order-service-gitops
- Create k8s/configmap.yaml with the following content:
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
data:
SQS_QUEUE_URL: "[[SQS_QUEUE_URL="https://sqs.us-east-1.amazonaws.com/123456789012/order-queue"]]"
AWS_REGION: "[[AWS_REGION="us-east-1"]]"
What does this ConfigMap do?
A ConfigMap stores non-secret configuration data as key-value pairs. Your Deployment will reference these keys to inject SQS_QUEUE_URL and AWS_REGION as environment variables into the container.
Replace the SQS_QUEUE_URL value with the queue URL you copied in the previous substep.
- Create k8s/deployment.yaml with the following content:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 2
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: [[ECR_IMAGE_URI="123456789012.dkr.ecr.us-east-1.amazonaws.com/order-service:v1"]]
ports:
- containerPort: 5000
env:
- name: SQS_QUEUE_URL
valueFrom:
configMapKeyRef:
name: order-service-config
key: SQS_QUEUE_URL
- name: AWS_REGION
valueFrom:
configMapKeyRef:
name: order-service-config
key: AWS_REGION
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 10
periodSeconds: 30
What does this Deployment do?
The Deployment tells Kubernetes to run 2 replicas of your container image. Each pod gets environment variables injected from the ConfigMap you just created.
The readinessProbe checks /health every 10 seconds to know when the pod can accept traffic. The livenessProbe checks every 30 seconds to restart unhealthy pods automatically.
Replace the image value with your actual ECR image URI from Step 2.
- Create k8s/service.yaml with the following content:
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
type: LoadBalancer
selector:
app: order-service
ports:
- protocol: TCP
port: 80
targetPort: 5000
What does this Service do?
A Service exposes your pods to network traffic. The LoadBalancer type tells k3s to bind port 80 on the host to port 5000 on your pods.
This is how traffic hitting your EC2 instance's public IP on port 80 reaches the Flask container. k3s includes a built-in ServiceLB that makes this work without an external load balancer.
- Create k8s/argocd-app.yaml with the following content:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: order-service
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/[[GITHUB_USERNAME="your-github-username"]]/order-service-gitops.git
targetRevision: main
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
What does this ArgoCD Application do?
This resource tells ArgoCD where to find your manifests (the k8s path in your GitHub repo) and where to deploy them (the default namespace in your cluster).
The automated sync policy means ArgoCD will automatically detect changes in your repo and apply them. The selfHeal flag ensures the cluster state is corrected if someone changes it manually.
- Verify all four files exist by running:
ls k8s/
You should see argocd-app.yaml, configmap.yaml, deployment.yaml, and service.yaml listed.
✔️ Awesome, I've got everything!
Great. Double check you have all four YAML files saved in the k8s/ directory with your actual values filled in.
ⓧ I'd like to double check the full code
Here are all four manifest files for reference. Make sure your values (account ID, region, queue URL, GitHub username) are substituted.
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
data:
SQS_QUEUE_URL: "https://sqs.REGION.amazonaws.com/ACCOUNT_ID/order-queue"
AWS_REGION: "us-east-1"
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 2
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/order-service:v1
ports:
- containerPort: 5000
env:
- name: SQS_QUEUE_URL
valueFrom:
configMapKeyRef:
name: order-service-config
key: SQS_QUEUE_URL
- name: AWS_REGION
valueFrom:
configMapKeyRef:
name: order-service-config
key: AWS_REGION
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 10
periodSeconds: 30
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
type: LoadBalancer
selector:
app: order-service
ports:
- protocol: TCP
port: 80
targetPort: 5000
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: order-service
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/YOUR_USERNAME/order-service-gitops.git
targetRevision: main
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
Files missing or empty?
If a file is missing, make sure you are in the ~/order-service-gitops directory when creating files. Run pwd to confirm your current location.
If a file is empty, check that you saved it correctly in your text editor. You can view file contents with cat k8s/configmap.yaml to verify.
Help me check my Kubernetes manifest files are correct.
Push the manifests to GitHub
ArgoCD needs a Git repository to watch for changes. You will create a dedicated GitOps repository on GitHub and push your manifests there.
- Open github.com in your browser and sign in.
- Click the + icon in the top right corner, then select New repository.
- Enter order-service-gitops as the repository name.
- Set the visibility to Public.
- Leave all other options unchecked (no README, no .gitignore, no license).
- Click Create repository.
Why a separate GitOps repo?
Keeping your Kubernetes manifests in a separate repository from your application source code is a GitOps best practice. It gives ArgoCD a clean, single-purpose repo to watch and allows different teams or pipelines to update manifests independently.
- Back in your EC2 SSH session, make sure you are in the ~/order-service-gitops directory:
cd ~/order-service-gitops
- Initialize the repository, add your manifests, and push to GitHub by running these commands:
git init
git add .
git commit -m "Add Kubernetes manifests"
git branch -M main
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/order-service-gitops.git
git push -u origin main
What do these commands do?
- git init creates a new local Git repository in the current directory.
- git add . stages all files in the k8s/ directory.
- git branch -M main renames the default branch to main (matching what ArgoCD expects in your argocd-app.yaml).
- git push -u origin main uploads your manifests to GitHub.
Git authentication from EC2
When you push, Git will ask for your credentials. Use your GitHub username and a Personal Access Token (not your GitHub password). You can create a token at github.com/settings/tokens with the repo scope.
- Verify the push by refreshing your order-service-gitops repository page on GitHub.
You should see a k8s/ directory containing all four manifest files.
Push failed or permission denied?
Make sure you created the repository as Public and that you entered the correct Personal Access Token when prompted. Tokens need the repo scope to push to repositories.
If you see a "repository not found" error, double check the remote URL matches your actual GitHub username and repository name. Run git remote -v to inspect it.
Why is my git push to GitHub failing from EC2?
Your SQS queue is ready and your Kubernetes manifests are safely stored in GitHub. Next up, you will install ArgoCD into your cluster and point it at this repository to deploy your application automatically.
Install ArgoCD and Deploy via GitOps
In the previous step, you pushed your Kubernetes manifests to a GitHub repository. Your cluster has everything it needs to run the order service, but right now there is no automation connecting your Git repo to the cluster.
In this step, you will install ArgoCD to watch your GitOps repository and automatically deploy changes to your k3s cluster. You will then verify the full pipeline by sending a live order request through to SQS.
In this step, get ready to:
- Install ArgoCD v3.4.3 and its CLI on your EC2 instance.
- Create an ArgoCD Application that syncs your GitOps repo to the cluster.
- Test end-to-end by sending an order and verifying it reaches SQS.
Install ArgoCD into the cluster
ArgoCD is a declarative GitOps continuous delivery tool for Kubernetes. It watches a Git repository and keeps your cluster in sync with whatever manifests are committed there.
- Create the argocd namespace and install the ArgoCD server components by running these commands:
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.4.3/manifests/install.yaml
What do these commands do?
The first command creates a dedicated namespace for ArgoCD's components. The second command downloads and applies the official ArgoCD v3.4.3 manifests into that namespace, deploying the API server, repo server, application controller, and other components.
- Wait for the ArgoCD pods to become ready by running:
kubectl wait --for=condition=available deployment -n argocd --all --timeout=120s
- Verify that all ArgoCD pods are running:
kubectl get pods -n argocd
You should see several pods (argocd-server, argocd-repo-server, argocd-application-controller, etc.) all showing Running status.
Pods stuck in Pending or CrashLoopBackOff?
If pods are stuck in Pending, your EC2 instance may be running low on resources. Confirm you launched a t3.medium (4 GiB RAM). If pods are in CrashLoopBackOff, run kubectl logs <pod-name> -n argocd to see the error details.
Help me troubleshoot ArgoCD pods that aren't running on my k3s cluster.
- Install the ArgoCD CLI on the EC2 instance by running these commands:
curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd
rm argocd-linux-amd64
What do these commands do?
The first command downloads the ArgoCD CLI binary for Linux. The second installs it to /usr/local/bin/argocd with executable permissions. The third removes the downloaded file to keep things clean.
- Retrieve the initial admin password by running:
argocd admin initial-password -n argocd
Copy the password that is printed to your terminal. You will use it to log in next.
- Log into the ArgoCD CLI using the admin credentials by running:
argocd login --port-forward-namespace argocd --username admin --password [[ARGOCD_PASSWORD="the password you just copied"]] --insecure
Why use --port-forward-namespace?
The ArgoCD API server is only exposed inside the cluster. The --port-forward-namespace argocd flag tells the CLI to open a temporary port-forward to reach the server. The --insecure flag skips TLS verification since the server uses a self-signed certificate.
You should see a message confirming you are logged in successfully.
Login timing out?
If the login hangs, the ArgoCD server pod may not be fully ready. Run kubectl get pods -n argocd and confirm the argocd-server pod shows 1/1 Running. If it is still starting, wait a minute and try again.
Help me troubleshoot my ArgoCD CLI login timing out.
Create the ArgoCD Application and sync
With ArgoCD installed and authenticated, you can now create an Application resource that tells ArgoCD where to find your manifests and where to deploy them.
- Apply the ArgoCD Application manifest from your GitOps repo by running:
kubectl apply -f k8s/argocd-app.yaml
What does this manifest do?
The argocd-app.yaml you pushed in the previous step defines an ArgoCD Application named order-service. It points to your GitHub repo's k8s directory on the main branch.
The syncPolicy.automated block enables automatic sync with prune (removes resources deleted from Git) and selfHeal (reverts manual drift in the cluster).
- Trigger the initial sync to deploy your application by running:
argocd app sync order-service --port-forward-namespace argocd
- Wait for the pods to start, then verify the application status by running:
argocd app get order-service --port-forward-namespace argocd
You should see Sync Status: Synced and Health Status: Healthy in the output.
- Confirm the pods are running by checking:
kubectl get pods
You should see 2 pods for order-service in Running state (matching the 2 replicas defined in your deployment).
Pods showing ImagePullBackOff?
This means k3s cannot pull your ECR image. Verify that the ECR image URI in deployment.yaml matches your actual account ID and region. Also confirm the EC2 instance's IAM role has the AmazonEC2ContainerRegistryReadOnly policy attached.
Help me fix ImagePullBackOff for my ECR image on k3s.
Test the deployment end-to-end
The order service is now live on port 80 of your EC2 instance, thanks to the k3s ServiceLB binding your LoadBalancer Service to the host. Time to send a real order and confirm it flows through to SQS.
- Send a test order to your service by running:
curl -X POST http://[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]/orders \
-H "Content-Type: application/json" \
-d '{"item": "widget", "qty": 3}'
You should receive a JSON response with a 201 status containing a message_id field. This confirms the Flask API received the order and published it to SQS.
- Verify the message landed in your SQS queue by running:
aws sqs receive-message --queue-url [[SQS_QUEUE_URL="https://sqs.REGION.amazonaws.com/ACCOUNT_ID/order-queue"]]
You should see the message body containing your order data ({"item": "widget", "qty": 3}).
What just happened?
Your request hit the EC2 instance on port 80. The k3s ServiceLB forwarded it to one of the order-service pods on port 5000. The Flask app parsed the JSON, published it to SQS using the EC2 instance's IAM role (no hardcoded credentials), and returned the message ID.
This is a fully GitOps-managed deployment. ArgoCD watches your repo and will automatically redeploy whenever manifests change.
Getting Connection refused on port 80?
Check that your security group allows inbound traffic on port 80. Also run kubectl get svc to confirm the order-service Service shows an external IP assigned by k3s ServiceLB.
Help me troubleshoot connection refused when curling my k3s service on port 80.
Your GitOps pipeline is fully operational. ArgoCD is watching your repository, your pods are running, and orders are flowing through to SQS. Next up, you will push a code change and watch ArgoCD automatically deploy it in the Secret Mission.
Secret mission
Trigger a GitOps Deployment
Your GitOps pipeline is set up, but you haven't seen the magic yet. What happens when you push a change to your GitOps repo? In this mission, you'll modify your application, rebuild the image, update the manifest, and watch ArgoCD automatically roll out the new version without you touching kubectl.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The EC2 instance costs $0.0418/hr (about $1/day), while SQS and ECR usage stays within the free tier for this project.
Cost warning
Your t3.medium EC2 instance charges $0.0418/hr as long as it is running. If you leave it on overnight, that is roughly $1/day.
If you are not actively using the cluster, stop or terminate the instance to avoid unexpected charges.
Resources you used:
- EC2 instance (t3.medium) running k3s, Docker, and ArgoCD.
- ECR repository (order-service) storing container images v1 and v2.
- SQS queue (order-queue).
- IAM role (order-service-role) with attached policies.
- Security group (order-service-sg).
- GitHub repository (order-service-gitops).
Keep everything running
No action needed. Choose this if you are still actively building or plan to continue to Project 2 (Argo Rollouts for blue/green deployments).
- Your EC2 instance will continue running at $0.0418/hr (roughly $1/day).
- SQS and ECR remain within free tier limits for normal usage.
- Your ArgoCD dashboard and GitOps pipeline stay active.
Pause - I'll come back to this later
Stop the EC2 instance to eliminate compute charges while keeping your setup intact. You only pay for EBS storage (about $0.08/month for 20 GB).
- Stop the EC2 instance by running this from your local terminal:
- When you are ready to resume, start the instance again from the AWS Console. Note that the public IP will change unless you have an Elastic IP attached.
aws ec2 stop-instances --instance-ids [[EC2_INSTANCE_ID="your EC2 instance ID (e.g. i-0abc123def456)"]]
You can find your instance ID in the EC2 Console under Instances.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow these steps in order.
- Terminate the EC2 instance by running:
aws ec2 terminate-instances --instance-ids [[EC2_INSTANCE_ID="your EC2 instance ID (e.g. i-0abc123def456)"]]
This permanently destroys the instance along with k3s, Docker, and ArgoCD installed on it.
- Delete the ECR repository and all its images by running:
aws ecr delete-repository --repository-name order-service --force
- Delete the SQS queue by running:
aws sqs delete-queue --queue-url [[SQS_QUEUE_URL="your SQS queue URL (e.g. https://sqs.us-east-1.amazonaws.com/123456789012/order-queue)"]]
- Delete the IAM role. You need to detach its policies first, then remove it from the instance profile, then delete both:
aws iam detach-role-policy --role-name order-service-role --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
aws iam detach-role-policy --role-name order-service-role --policy-arn arn:aws:iam::aws:policy/AmazonSQSFullAccess
aws iam remove-role-from-instance-profile --instance-profile-name order-service-role --role-name order-service-role
aws iam delete-instance-profile --instance-profile-name order-service-role
aws iam delete-role --role-name order-service-role
- Delete the security group (wait a few minutes after terminating the instance for the network interface to release):
aws ec2 delete-security-group --group-id [[SG_GROUP_ID="your security group ID (e.g. sg-0abc123def456)"]]
- Delete the GitHub repository. Go to your order-service-gitops repository on GitHub.
- Click Settings.
- Scroll to the bottom and click Delete this repository.
- Confirm the deletion by typing the repository name when prompted.
That's a Wrap!
That's a Wrap!
Nice work! You just built and deployed a production-grade cloud-native microservice using a modern GitOps workflow.
You've learned how to:
- Built a containerized Python Flask order-processing API and pushed the image to Amazon ECR.
- Launched an EC2 instance running k3s (a single-node Kubernetes cluster) and configured Amazon SQS access via an EC2 instance IAM role with no hardcoded credentials.
- Installed ArgoCD and deployed the application declaratively via GitOps, with automated sync from a GitHub repository.
- Secret Mission: Triggered an automated deployment by pushing a manifest change and watching ArgoCD sync the new version live.
Ready to quiz yourself?