Deploy to EC2 from GitHub Actions without opening port 22
Ankur KDev.to (EN Zone)
1 views
If you deploy to EC2 from GitHub Actions, the usual recipe is to put a private key in your repo secrets and SSH into the box. It works, but it means you are keeping a long-lived key around and leaving port 22 open to the internet (or to GitHub's very large IP range). AWS has a better answer for this: Systems Manager Run Command. The SSM Agent on your instance makes an outbound connection to AWS, and you send commands through the SSM API. There is no inbound port, no key to rotate, and every command is recorded in CloudTrail. Your instance can sit in a private subnet with no public IP at all and this still works.
I wanted to use this in my own pipelines. I looked around the marketplace and could not find a single action that did it well. Some were thin wrappers around aws ssm send-command that fired the command and never checked whether it actually succeeded. Some did poll, but swallowed the remote exit code, so a failed deploy showed up as a green build. Others hit the SSM output limit (roughly 24 KB) and truncated my logs right at the interesting part. A few were simply abandoned.
So I wrote one: ankurk91/aws-ssm-run-command-action.
SSM vs SSH
Both get the job done. Here is how they actually compare for CI/CD:
SSM Run Command
SSH
Inbound port 22
Not needed
Required (or a bastion)
Public IP on the instance
Not needed
Usually needed
Credentials in CI
IAM role via OIDC, short-lived
Long-lived private key in secrets
Key rotation
Nothing to rotate
You own the whole rotation dance
Instance in a private subnet
Works (NAT or VPC endpoints)
Needs a bastion or VPN
Who can run what
IAM policies, scoped per instance or tag
authorized_keys on each box
Audit trail
CloudTrail + SSM command history
Whatever auth.log kept
Revoking access
Detach the IAM policy
Edit files on every server
Live output streaming
No, you poll and fetch
Yes
File copy (scp / rsync)
No
Yes
Server-side setup
SSM Agent + an IAM role
sshd + key distribution
SSM wins on everything that matters for security and access control. SSH keeps two real advantages: live output streaming and file transfer. For a deploy script I have not missed either. The full log lands in S3 anyway, and it is usually better to have the server pull its build artifacts from S3 or a registry than to push them over scp from a runner.
Using the action
You need three things on the AWS side:
The SSM Agent on the instance. Amazon Linux and the official Ubuntu AMIs already ship with it.
An IAM role attached to the instance with the AmazonSSMManagedInstanceCore managed policy.
A private S3 bucket for logs. This is how the action gets around the 24 KB output limit. Add a lifecycle rule to delete old objects and forget about it.
Then the workflow:
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- name: Run commands on EC2
uses: ankurk91/aws-ssm-run-command-action@v1
with:
ec2_instance_id: ${{ vars.EC2_INSTANCE_ID }}
run_as_user: ubuntu
log_bucket_name: ${{ vars.LOG_BUCKET_NAME }}
commands: |
set -e
cd /var/www/app
git pull --ff-only
npm ci
npx prisma migrate deploy
npm run build
pm2 reload ecosystem.config.js --update-env
That is the whole thing. No secrets other than the AWS role, no port 22.
A few notes from using this in production:
Start your script with set -e. Without it the shell keeps going after a failed command and reports success.
The action exposes a command-exit-code output, so you can branch on it if you need to.
execution_timeout defaults to one hour. Lower it for a normal deploy so a hung command does not sit there burning runner minutes.
Full output lands in your S3 bucket, so nothing gets cut off.
The pipeline itself only needs ssm:SendCommand, ssm:ListCommandInvocations and ssm:GetCommandInvocation. The full policy is in the repo.
Bonus: port forwarding through SSM
Run Command is for firing off a script on the server. Sometimes you want a network connection instead. In the deploy above, prisma migrate deploy runs on the EC2 instance, which is fine. But you may prefer to run migrations from the runner, so that a bad migration fails the pipeline before any new code goes out.
That needs the runner to reach your database, and your database is almost certainly in a private subnet. Session Manager port forwarding solves it, and enkhjile/aws-ssm-remote-port-forwarding-action wraps it up nicely. It closes the session in its post step, so there is nothing for you to clean up.
- name: Open a tunnel to RDS
uses: enkhjile/aws-ssm-remote-port-forwarding-action@v1
with:
target: ${{ vars.EC2_INSTANCE_ID }}
host: my-db.abc123.ap-south-1.rds.amazonaws.com
port: 5432
local-port: 5432
- name: Run Prisma migrations through the tunnel
run: |
npm ci
npx prisma migrate deploy
env:
DATABASE_URL: postgresql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@127.0.0.1:5432/app?schema=public
Prisma just sees a database on localhost. Your EC2 instance is the jump host, but you never log into it, and neither the instance nor the RDS security group needs an inbound rule from the internet.
Wrapping up
If you are still shipping a private key to GitHub secrets to deploy to EC2, SSM is worth an afternoon of your time. You delete the key, close the port, and get an audit log for free.
Links:
The action: github.com/ankurk91/aws-ssm-run-command-action
Port forwarding action: github.com/enkhjile/aws-ssm-remote-port-forwarding-action
AWS docs:
What is AWS Systems Manager
AWS Systems Manager Run Command
Install the SSM Agent on Linux
IAM instance profile for Systems Manager
Port forwarding with Session Manager
VPC endpoints for Systems Manager
If you try the action and something is missing, open an issue on the repo. Stars are appreciated too.
Is there a light weight tool to monitor and log very high load processes? About once a week for the past couple months my RockPi4 gets extremely high load and stops responding to even SSH login requests. I'd like to run something on that same machine that will log what is generating that load. It do
I have been using SSH for a very long time.
SSH itself is not the problem.
OpenSSH is mature, ubiquitous, scriptable, compatible with just about everything, and already knows how to deal with keys, agents, jump hosts, port forwards, certificates, host verification and all the other things accumula
Hello internet, I am trying to set up a home server where I can store photos and ideally still access them from my phone. I am not knowledgable at all in tech but am willing to learn. I thought I could use an external harddrive to run store all the files and programs to run it and just plug it into