Problem#
Publish a static site on AWS without storing access keys in the pipeline and without opening SSH to the GitLab runners. The application is the HTML and CSS course site by Gustavo Guanabara; the focus of the project is the infrastructure and the pipeline, not the site.
Project architecture#
- Terraform creates a t3.micro EC2 instance in a new subnet, inside an existing VPC. The security group allows only SSH, from one IP address.
- ECR stores the
ecr-site-dandroidimage, built from aDockerfilebased onnginx:alpine. - The Terraform state lives in an S3 bucket, with native locking.
Technical decisions and reasoning#
- SSM deploy instead of SSH from the pipeline: the shared GitLab runners have variable IPs. Opening port 22 for them would require exposing it to the internet. With SSM, the deploy uses the AWS API and no inbound port is needed.
- OIDC instead of an access key: GitLab assumes a role through OIDC and receives temporary credentials. No long-lived key is stored.
- Access only through an SSH tunnel: port 8080 on the EC2 instance is not published to the internet. To view the site, you open an SSH tunnel. Domain and HTTPS are not configured.
- Image tag from the commit SHA: ECR uses mutable tags. Using
latestwould let each build overwrite the previous one. - Saved plan, then apply: the
planjob generates atfplan, and theapplyjob applies that same file. What was reviewed is what gets applied. - Manual apply, on one branch only:
applyis manual and configured for themes-11-projectsbranch.
Real challenges#
- Dependency on the first run: the
buildjob pushes the image to an ECR repository that only exists afterapply. On the first run,buildfails. The README advises settingallow_failureon that job for that run and removing it afterwards.
What I would do differently#
- HTTPS with a domain: today the site is only reachable through an SSH tunnel. With a domain and a certificate, on the host or behind an ALB with ACM, it could be public.
- Docker in user_data: the deploy installs Docker on every run. Installing it in user_data would leave the instance ready when it starts.
- Role and bucket as variables: the role name and the state bucket are hardcoded, which makes the project harder to reproduce in another account.
- Least privilege on the role: the role creation policy uses
Resource: "*". I would restrict it by ARN and by tag. - Apply from main: today
applyonly runs on the working branch. In a real workflow, it would run from main after review.
Technologies#
Terraform, AWS (EC2, ECR, S3, IAM, SSM, VPC), Docker, nginx, GitLab CI, OIDC.