There is a question this course has not asked yet, and it is worth asking bluntly: where is
MercadoFresco's code? The honest answer is that it is on Luis's laptop, in ~/proyectos/tienda, with a
git init from fourteen months ago and 1,847 commits nobody else has ever seen. There is a compressed
copy on a USB stick from September. Marta has a folder called tienda-v2-FINAL and does not know whether
it is older or newer. And when Luis deploys, he scps the whole folder to an instance of the ASG
asg-mercadofresco-tienda and restarts the service by hand.
Everything built over seven modules rests on source code that lives on a single hard disk with no reliable backup. This module fixes that, and it starts at the beginning: where the code lives and how changes are governed.
Cost warning. CodeCommit charges 1 USD per active user and month from the sixth onwards, with 50 GB and 10,000 Git requests included: for MercadoFresco's team of three, zero. CodeConnections connections to GitHub or GitLab are free too; you pay for the downstream usage, which we will see in 08-02 and 08-04. All the data and credentials in this lesson are fictitious.
Contents
- The starting point: a repository on a laptop
- What a managed Git repository gives you
- Where CodeCommit stands, without the varnish
- Comparison: CodeCommit, GitHub, GitLab and Bitbucket
- CodeConnections: the bridge between AWS and your Git provider
- MercadoFresco's repositories
- Authentication: HTTPS, SSH, the CLI credential helper and roles
- IAM policies: who can push to
main - Branching strategy: trunk-based versus GitFlow
- MercadoFresco's branching flow
- Pull requests: review, approval and merge
- Approval rules and protecting
main - Conventions: commits, semantic versioning and tags
- The
.gitignoreand why you never commit a.env - Notifications and triggers
- Migrating Luis's repository
- Cost and cleanup
- Common mistakes and tips
- Exercises
- Conclusion
The starting point: a repository on a laptop
Marta drew up a risk inventory before touching anything:
| Risk | Current situation | Consequence |
|---|---|---|
| Losing the laptop | USB copy from 5 months ago | 5 months of work lost |
| Two people touch the same thing | They warn each other on chat | Silent overwrite |
| Knowing what is in production | "Whatever I uploaded on Friday" | Nobody can reproduce a failure |
| Going back to the previous version | Search the recycle bin | Minutes or hours of downtime |
| Reviewing before it goes in | Not done | Errors seen in production |
| Secrets in the code | There is a .env with the RDS password |
Leak when sharing the repository |
The first four are solved by any remote repository. The last three also need governance: rules about who can do what and under what conditions a change goes in. That distinction is the whole lesson: hosting the code is the easy part.
What a managed Git repository gives you
Git is distributed and nothing forces you to have a server, but every team designates one copy as the reference, and that copy needs to be available, backed up and to control who writes to it.
| Aspect | Your own server (EC2 + Gitea/GitLab CE) | Managed |
|---|---|---|
| Durability | That of EBS; you do the backups | Replicated across AZs, with no action from you |
| Availability | One instance = a single point of failure | The provider's service level agreement |
| Encryption at rest | You configure it | Automatic; in CodeCommit, with KMS |
| Identity | System users or your own LDAP | IAM (CodeCommit) or OIDC/SAML |
| Patching and CVEs | Yours | The provider's |
| Real cost | Instance + EBS + your time | Per user or included |
| Auditing | Server logs, if you keep them | CloudTrail or the provider's log |
For three people, running a Git server is time that does not go into the product: MercadoFresco does not
set one up. CodeCommit's encryption at rest deserves a paragraph, because it connects with module 4:
it always encrypts, with no option to turn it off, and it can do so with a customer key. Here that means
reusing alias/mercadofresco-datos, the same key that already encrypts Aurora and the buckets: one key,
one policy, one rotation and a single place to revoke access. That is the concrete advantage of the
integration: there is no second encryption system to govern.
Where CodeCommit stands, without the varnish
Since July 2024, AWS has not accepted new customers on CodeCommit. If your account was already using it before, it keeps working with support and security fixes. If it is new, the console will not let you create repositories. No end-of-life date has been announced, but there are no new features either: the official recommendation for new projects is an external Git provider connected with CodeConnections.
Three practical consequences: you are going to run into it in companies that adopted it between 2015 and 2024 with hundreds of repositories, so knowing how to operate it is useful; do not choose it for a new project, not even if your old account lets you, because you would be building on a service with no evolution; and what matters here is not the service, it is the concepts — branches, pull requests, approval rules, branch protection, commit and versioning conventions are identical in GitHub, GitLab or Bitbucket, and they are what 08-02, 08-03 and 08-04 will consume.
Marta makes the decision you would make today: the code goes to GitHub, and AWS connects with CodeConnections. The lesson goes on explaining CodeCommit because you will run into it and because its permission model — pure IAM — is didactically very clear.
Comparison: CodeCommit, GitHub, GitLab and Bitbucket
| Criterion | CodeCommit | GitHub | GitLab | Bitbucket |
|---|---|---|---|---|
| New customers | Not accepted | Yes | Yes | Yes |
| Identity | Native IAM | Its own; paid SSO | Its own; SSO | Atlassian |
| Code review | Functional but poor | The best on the market | Very complete | Adequate |
| Own CI/CD | No (uses CodeBuild) | GitHub Actions | GitLab CI | Bitbucket Pipelines |
| AWS integration | Fully native | CodeConnections + OIDC | CodeConnections + OIDC | CodeConnections |
| Self-hostable | No | Enterprise Server | Yes, self-managed | Data Center |
| Ecosystem | None | Enormous | Broad and integrated | Tied to Jira |
| Code residency | Your AWS account and region | GitHub's servers | GitLab or your server | Atlassian |
| Cost 3 / 30 people | 0 / ~25 USD month | 0 / ~120 USD month | 0 / ~870 USD month | 0 / ~90 USD month |
Choice criteria, in the order in which they must be applied:
- Is there a legal or contractual requirement that the code must not leave your account or the EU? If it is non-negotiable, your options are CodeCommit (only if you already had it) or self-hosted GitLab. It is the only criterion that overrides all the others.
- Where is your team already? Moving people who have spent years on GitHub has a daily friction cost that you pay forever. This criterion decides most cases.
- Will you use the provider's CI/CD or the AWS one? Actions and GitLab CI are excellent and deploy to AWS with OIDC and no keys; if you prefer building and deploying inside AWS — with its roles, its VPCs and its bill — CodeConnections gives you the best of both.
- How much ecosystem do you need? In dependency scanning, issues and boards, GitHub and GitLab win without argument.
MercadoFresco: the team is already on GitHub, there is no restriction on the source code (customer
data is restricted to eu-west-1, but that is another matter) and they want building and deployment in
AWS to reuse roles and alarms. GitHub + CodeConnections + CodeBuild/CodeDeploy/CodePipeline.
CodeConnections: the bridge between AWS and your Git provider
CodeConnections — until 2023, AWS CodeStar Connections; you will see both names in the documentation and in old ARNs — is an AWS resource representing an OAuth authorisation between your account and a GitHub, GitLab or Bitbucket organisation. Once created, CodePipeline or CodeBuild read the repository and receive its events without you storing any token anywhere.
It is created in two steps, and the second one is necessarily manual:
# Step 1: create the connection. It is born PENDING and is not usable yet.
aws codeconnections create-connection \
--provider-type GitHub \
--connection-name conn-mercadofresco-github \
--tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
Key=Componente,Value=cicd Key=Propietario,Value=marta Key=CentroCoste,Value=plataforma \
--profile mercadofresco-dev --region eu-west-1Step 2, in the console and only in the console. Developer Tools → Settings → Connections, select the pending connection and click "Update pending connection". GitHub's OAuth flow opens, the AWS Connector for GitHub app is installed in the organisation and the authorised repositories are chosen. This step cannot be automated by design: it is a human authorisation in an external provider, not a call to the AWS API. If a CloudFormation template "creates ready-made connections", it creates the resource and somebody authorises it by hand once.
Check with aws codeconnections list-connections that the status is AVAILABLE before moving on: a
connection in PENDING is the number one cause of a pipeline's source action failing with an
uninformative permissions error, and we will come back to it in 08-04.
Grant access only to the repositories you need. GitHub offers to give access to the whole organisation; choose "Only select repositories" and tick MercadoFresco's two. It is the least privilege of 04-01 applied outside AWS.
MercadoFresco's repositories
| Repository | Contents | Who touches it | Cadence |
|---|---|---|---|
mercadofresco-tienda |
Web, API, queue consumers, Lambda code, tests, buildspec.yml, appspec.yml |
Luis | Several times a day |
mercadofresco-infra |
Infrastructure definition, operations scripts, runbooks | Marta | Weekly |
Separating them makes it possible to deploy the application twenty times a day without dragging
infrastructure changes along, and to subject a network change to stricter review. We will fill in the
contents of mercadofresco-infra in module 9: today only the repository exists.
# In CodeCommit
aws codecommit create-repository \
--repository-name mercadofresco-tienda \
--repository-description "MercadoFresco web application, API and consumers" \
--kms-key-id alias/mercadofresco-datos \
--tags Proyecto=mercadofresco,Entorno=produccion,Componente=codigo,Propietario=luis,CentroCoste=plataforma \
--profile mercadofresco-dev --region eu-west-1
# In GitHub (the chosen option)
gh repo create mercadofresco/mercadofresco-tienda --private
gh repo create mercadofresco/mercadofresco-infra --private--kms-key-id is where encryption with your own key materialises: if one day you revoke that key, the
repository stops being readable, which is exactly what you want in the face of a leak. And the repository
is always private, with no "public for now, we will change it later": the crawlers that look for
credentials in public repositories take minutes, not days.
Authentication: HTTPS, SSH, the CLI credential helper and roles
Here is CodeCommit's most interesting conceptual difference: there are no Git users, there are IAM identities. There is no "user luis" on the server with his password; there is his IAM user or role, and permission to push is a policy. Removing somebody's access means removing IAM, not remembering a second system.
| Method | How it works | When | Drawback |
|---|---|---|---|
| IAM HTTPS credentials | Username/password generated in IAM | Laptop with an IAM user | Long-lived material |
| SSH keys | Public key on the IAM user | Laptop, if you prefer SSH | The same |
| Credential helper | Git asks the CLI for a SigV4 signature | Recommended: SSO, MFA, roles | Requires the CLI |
| IAM role | The machine assumes a role, no credentials | CodeBuild, EC2, Lambda | Machines only |
Use the credential helper. Its virtue is that it stores nothing: every time Git needs to
authenticate it invokes aws codecommit credential-helper, which signs with the profile's current
credentials. If they come from IAM Identity Center and expire after eight hours, access expires with them.
It is the temporary credentials principle of 04-01 applied to the repository.
git config --global credential.helper '!aws --profile mercadofresco-dev codecommit credential-helper $@'
git config --global credential.UseHttpPath true
git clone https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/mercadofresco-tiendacredential.UseHttpPath true is mandatory and constantly forgotten: without it, Git does not send the
repository path, the signature is computed over the wrong resource and you get a 403 that explains nothing.
In GitHub, the modern equivalent for machines is OIDC, not an access key. Instead of storing an
AWS_ACCESS_KEY_ID as a repository secret, trust is established between GitHub's OIDC provider and a role
in your account, with a condition on the specific organisation and repository:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:mercadofresco/mercadofresco-tienda:*" }
}
}]
}The sub condition stops any repository in the world from assuming your role. A StringLike with
repo:* is a serious security hole that has been seen in production more than once.
IAM policies: who can push to main
In CodeCommit, branch protection is expressed as an IAM policy, through the codecommit:References
condition. This policy denies pushing directly to main:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenegarEscrituraDirectaEnMain",
"Effect": "Deny",
"Action": ["codecommit:GitPush", "codecommit:DeleteBranch",
"codecommit:PutFile", "codecommit:DeleteFile"],
"Resource": "arn:aws:codecommit:eu-west-1:111122223333:mercadofresco-tienda",
"Condition": {
"StringEqualsIfExists": { "codecommit:References": ["refs/heads/main"] },
"Null": { "codecommit:References": "false" }
}
}]
}Three details that are often copied wrongly. StringEqualsIfExists limits the denial to what affects
refs/heads/main: a push to funcionalidad/franja-horaria does not match and is allowed.
Null: {"codecommit:References": "false"} means "apply only if the key is present"; without it you
would also deny operations with no branch reference, blocking things you did not intend. And PutFile
and DeleteFile close the web console's door, which allows editing without going through Git and is the
most forgotten hole — by contrast, do not deny the MergeBranchesBy* actions: they are the ones a PR
merge executes, and blocking them prevents even approved PRs from being closed.
Deny always beats Allow, as we saw in 04-01: this is effective even if Luis has
AWSCodeCommitPowerUser attached. In GitHub the mechanism changes — branch protection rules or
rulesets — but the intent is identical.
Branching strategy: trunk-based versus GitFlow
| Aspect | Trunk-based | GitFlow |
|---|---|---|
| Long-lived branches | One: main |
main + develop + release/* + hotfix/* |
| Life of a working branch | Hours or 1-2 days | Days or weeks |
| Merge conflicts | Small and frequent | Large and painful |
| Mental complexity | Low | High; it has to be documented |
| Continuous delivery | It is its premise | Badly: the release branch gets in the way |
| Half-finished features | Hidden behind flags | Isolated in their branch |
| Simultaneous versions | Hard | It is what it solves well |
GitFlow was designed in 2010 for software that is published in versions: an installer, a library, a product the customer updates when they want. There it makes sense to maintain 1.4 while you develop 1.5. Its own author later published a note saying that if you deliver continuously — a website, a SaaS — you probably do not need it.
MercadoFresco is a web shop: there is only one version, the one in production right now. A release/*
branch adds nothing, and a develop that lives for weeks guarantees painful merges between three people
who touch the same files.
Marta's decision: trunk-based with nuances. main always deployable, short feature branches, and a
desarrollo branch that exists for one temporary reason: until the 08-04 pipeline is mature and the tests
inspire confidence, she wants a place where changes coexist for a day before reaching main. It is a
conscious compromise with a declared expiry date: scaffolding, not a cathedral.
MercadoFresco's branching flow
gitGraph
commit id: "v1.4.0"
branch desarrollo
checkout desarrollo
commit id: "sync"
branch funcionalidad/franja-horaria
checkout funcionalidad/franja-horaria
commit id: "order model"
commit id: "API + tests"
checkout desarrollo
merge funcionalidad/franja-horaria id: "PR 42"
commit id: "dev smoke OK"
checkout main
merge desarrollo tag: "v1.5.0"
branch correccion/iva-canarias
checkout correccion/iva-canarias
commit id: "fix VAT"
checkout main
merge correccion/iva-canarias tag: "v1.5.1"
checkout desarrollo
merge main id: "sync fix"
The written rules, which live in CONTRIBUTING.md:
| Branch | From | To | Life | Who merges |
|---|---|---|---|---|
main |
— | — | Permanent | Nobody directly |
desarrollo |
main |
main via PR |
Permanent | Marta, after review |
funcionalidad/<n> |
desarrollo |
desarrollo via PR |
Max. 3 days | Author, 1 approval |
correccion/<n> |
main |
main and desarrollo |
Hours | Author, 1 approval |
experimento/<n> |
Anywhere | Deleted | Days | Not merged |
The three-day limit is not bureaucracy: a branch that lives for two weeks diverges so much that
merging it stops being a merge and becomes a reintegration; if a change does not fit in three days, split
it. Urgent fixes come off main, not off desarrollo, which would drag half-finished changes into
production; and they have to be merged also into desarrollo, or the failure will reappear in the
next delivery. And branches are deleted on merge: a repository with 140 dead branches is a repository
where nobody finds anything.
Pull requests: review, approval and merge
A pull request is the proposal to merge one branch into another, with a space to discuss it before it happens. It is what turns "Luis uploaded something" into "the team accepted a change".
aws codecommit create-pull-request \
--title "Add delivery time slot to the order" \
--description "$(cat <<'EOF'
## What changes
Adds the `franja_entrega` field (manana|tarde|24h) to the order and propagates it to the
`PedidoConfirmado` event as an optional attribute.
## Why
Sara's request: 31 % of delivery incidents are caused by the customer being out.
## How to test it
1. POST /pedidos with "franja_entrega": "tarde"
2. Check the message in cola-mercadofresco-pedidos
3. Check that an order WITHOUT the field still works (backwards compatibility)
## Risks
Schema change in Aurora: NULLABLE column, no default value, no new index.
Old consumers ignore the attribute. See 08-05.
EOF
)" \
--targets repositoryName=mercadofresco-tienda,sourceReference=funcionalidad/franja-horaria,destinationReference=desarrollo \
--profile mercadofresco-dev --region eu-west-1That description is a real template and is worth copying. A PR with no description forces the reviewer to reconstruct the intent by reading the diff, the slowest and least reliable way to understand a change. The four sections are answered in three minutes and save the reviewer twenty.
Comments can be general or anchored to a specific line of the diff with post-comment-for-pull-request
and its --location filePath=app/pedidos.py,filePosition=147 parameter, which is where useful review
happens: on the line, not in a separate email.
The three merge strategies, identical on any platform:
| Strategy | What it does | Resulting history | When |
|---|---|---|---|
| Fast-forward | Moves the pointer, no new commit | Linear, keeps every commit | Branch of 1 commit already rebased |
| Squash | Flattens the branch into one commit | Linear and clean: 1 PR = 1 commit | Default for features |
| Three-way | Merge commit with two parents | Branched, keeps the detail | Between long-lived branches |
MercadoFresco uses squash for feature branches and three-way for desarrollo → main. The
reasoning: the fifteen commits of "wip", "fix the test" and "now it works" add nothing six months later,
and a git log of main where each line is a complete, described change is an excellent diagnostic tool
during an incident; when moving desarrollo into main, on the other hand, you want to keep the
individual commits so you can revert just one. One consequence of squashing that surprises people the
first time: the resulting commit is new and shares no identifier with the branch's commits, so the
branch looks "unmerged" to Git even though its content is inside. Delete it anyway.
aws codecommit merge-pull-request-by-squash \
--pull-request-id 42 --repository-name mercadofresco-tienda \
--commit-message "Add delivery time slot to the order (#42)" \
--profile mercadofresco-dev --region eu-west-1Approval rules and protecting main
A PR that anybody can merge without anybody looking at it is a formality. What turns it into a real control is an approval rule template: a policy applied automatically to PRs meeting a criterion, which demands approvals from a given set of people.
aws codecommit create-approval-rule-template \
--approval-rule-template-name plantilla-mf-main-requiere-aprobacion \
--approval-rule-template-description "Every PR into main needs 1 approval from the team" \
--approval-rule-template-content '{
"Version": "2018-11-08",
"DestinationReferences": ["refs/heads/main"],
"Statements": [{
"Type": "Approvers",
"NumberOfApprovalsNeeded": 1,
"ApprovalPoolMembers": ["arn:aws:sts::111122223333:assumed-role/rol-mercadofresco-desarrollo/*"]
}]
}' \
--profile mercadofresco-dev --region eu-west-1
# The template does nothing until it is associated with a repository
aws codecommit associate-approval-rule-template-with-repository \
--approval-rule-template-name plantilla-mf-main-requiere-aprobacion \
--repository-name mercadofresco-tienda \
--profile mercadofresco-dev --region eu-west-1DestinationReferences limits the rule to PRs into main: agility day to day, rigour at the door of
production. ApprovalPoolMembers accepts the assumed-role/<role>/* pattern, which is the right one
when the team signs in with SSO and identities are role sessions rather than IAM users.
NumberOfApprovalsNeeded: 1 is what makes sense for three people; with two, the process would jam as
soon as somebody went on holiday. And the author cannot approve their own PR: CodeCommit prevents it
natively and in GitHub you have to enable it, but it is half the value of the mechanism.
The second condition — that the build is green — cannot be expressed here, because these rules only count human approvals. There are two paths: CodeBuild approving the PR programmatically when it finishes successfully, or the check living in the Git provider as a required status check. This piece is closed in 08-04, when the pipeline exists and has something to say about the quality of the change.
Conventions: commits, semantic versioning and tags
Conventions look like cosmetics until the day a pipeline reads them and makes decisions with them.
The format is type(scope): description in the imperative — feat(pedidos): add delivery time slot,
fix(pagos): avoid double charge when the gateway returns 504, chore(deps): bump boto3:
| Type | Meaning | Effect on the version |
|---|---|---|
feat |
New feature | MINOR |
fix, perf |
Fix or improvement | PATCH |
refactor, docs, test, chore |
No functional change | None |
Any with BREAKING CHANGE: in the body |
Incompatible change | MAJOR |
Semantic versioning (MAJOR.MINOR.PATCH) is the other side of the same coin: v1.5.0 versus
v1.4.3 tells you what to expect. In a shop whose only client is your own front end, MAJOR matters
little; as soon as you publish an API consumed by the warehouse ERP, it matters a lot.
git tag -a v1.5.0 -m "Delivery time slot + catalogue cache"
git push origin v1.5.0
git log --oneline v1.4.3..v1.5.0The operational value is that the deployed artefact carries that number. When
mercadofresco-alb-latencia-alta fires at 19:20 on a Friday, the first question will be "what version is
in production?" and the second "what went in since the previous one?". With tags you answer in ten
seconds; without them, by reconstructing dates and hoping.
The .gitignore and why you never commit a .env
A correct .gitignore is not hygiene: it is leak control.
# Environment and secrets - NEVER in the repository
.env
.env.*
!.env.example
*.pem
*.key
credenciales*.json
# Python and build artefacts
__pycache__/
.venv/
.pytest_cache/
.coverage
dist/
node_modules/
# Local data: may contain personal data
*.csv
*.sql
volcado_aurora_*.dump
!datos/ejemplo_anonimo.csvThe !.env.example line un-excludes a template of variables with no values, which does need to be
there so that somebody new knows what configuration is required. And the last lines stop an Aurora dump
with real orders from ending up in the repository: that is a data protection incident, not an oversight.
Why the .env is the worst of all. On Luis's laptop there is one with the mfadmin password, the
payment gateway key and the ERP token. If it goes into the repository even once:
it stays in the history forever — deleting it in the next commit does not remove it, git show
recovers it, and really cleaning it requires rewriting history and force-pushing, breaking every clone —,
it is replicated in every clone, CI cache and provider copy, and the only valid response is to
rotate the credentials: if the secret was exposed it is compromised, however long the exposure lasted.
We built the solution in 04-03 and here we just have to use it: passwords in
mercadofresco/produccion/rds/mfadmin with automatic rotation every 30 days, and non-sensitive
configuration in Parameter Store under /mercadofresco/produccion/. The application reads them at
start-up with rol-mercadofresco-tienda. There is not a single secret value in the repository, only the
name of the secret, which is not sensitive. And the rotation of 04-03 gains a value here that was not
obvious then: it puts an automatic expiry date on the damage of a leak. In 08-02 we will add one more net:
a scan that fails the build if it detects something that looks like a credential.
Notifications and triggers
A repository nobody looks at is a repository where PRs wait four days.
| Mechanism | What it is | Typical cases |
|---|---|---|
| Triggers | Repository configuration: on push, invokes SNS or Lambda |
Automation tied to the push |
| Notifications | A rule on service events towards SNS or a chat | Telling people about PRs and comments |
| EventBridge | Every CodeCommit event reaches the default bus |
Content-based routing |
aws codecommit put-repository-triggers \
--repository-name mercadofresco-tienda \
--triggers '[{"name":"aviso-push-main",
"destinationArn":"arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco",
"branches":["main"],"events":["updateReference"]}]' \
--profile mercadofresco-dev --region eu-west-1And an EventBridge rule that takes advantage of everything from 07-03, with
source: ["aws.codecommit"], detail-type: ["CodeCommit Pull Request State Change"] and a filter on
detail.event to react to pullRequestCreated. If what you want is to tell people, do not build it with
a trigger: that is what notifications are for, with the formatting done and Slack integration without code.
There is an anti-pattern worth naming here: using a trigger to launch the deployment. It is tempting
and it works on day one, but there is no build, no tests, no approval, no versioned artefact and no way to
retry. It is Luis's scp with more steps. What should react to the push is a pipeline, and that is
08-04.
Migrating Luis's repository
The local repository has 1,847 commits, three branches and a .env tracked since the second commit.
Step 1: inventory. git branch -a, git log --oneline | wc -l and git count-objects -vH to know
what is there and how much it weighs.
Step 2: look for secrets in the history before uploading anything. This is not optional.
git log --all --full-history --oneline -- .env
git log --all -p -S "AKIA" --oneline # AWS access key
git log --all -p -S "BEGIN RSA PRIVATE KEY" --onelineLuis finds .env in 312 commits. Two paths: rewrite the history with git filter-repo if the
repository will be public or shared with third parties, or upload and rotate if it is private and the
team is small. MercadoFresco chooses the second, but rotation is non-negotiable and happens before the
push, with aws secretsmanager rotate-secret --secret-id mercadofresco/produccion/rds/mfadmin and
regenerating the gateway key and the ERP token in the provider's panel.
Step 3: clean the working tree.
git rm --cached .env # stops tracking it, does not delete it from disk
printf '.env\n.env.*\n!.env.example\n' >> .gitignore
git add .gitignore && git commit -m "chore(seguridad): stop tracking .env and add .gitignore"Steps 4 and 5: remote and upload, with git remote add origin git@github.com:mercadofresco/mercadofresco-tienda.git
followed by git push --mirror origin.
--mirror uploads every reference — branches, tags and notes — exactly as they are locally, which is
what you want in an initial migration. Two warnings: it is destructive at the destination (it deletes
on the remote whatever does not exist locally, so use it only against an empty repository and only the
first time), and it is never used again afterwards; day to day is git push origin <branch>. If you
prefer fine control, git push origin --all and git push origin --tags.
Step 6: verify with a clean clone, the step people skip and later regret. Clone into /tmp, count the
commits — there should be 1,847 —, check branches and tags, and confirm that .env is not there.
Steps 7 and 8: create desarrollo, protect main at the provider (PR mandatory, one approval, no
force push), and have Marta and Sara clone. The repository stops living on a laptop the day there are at
least two more copies and somebody has checked that they work.
Cost and cleanup
| Item | Cost at MercadoFresco |
|---|---|
| CodeCommit, 3 users and 240 MB | 0 USD (5 users and 50 GB included) |
| CodeConnections and GitHub with private repositories | 0 USD |
| Notifications via SNS | Cents |
| Total for governing the code | ≈ 0 USD/month |
The barrier to adopting this is never economic: it is habit. What costs money starts in 08-02.
# IRREVERSIBLE and with no recycle bin: clone with --mirror to a disk before running it
aws codecommit delete-repository --repository-name pruebas-borrar \
--profile mercadofresco-dev --region eu-west-1Common Mistakes and Tips
Choosing CodeCommit for a new project. It does not accept new customers and receives no new features. Learn its concepts, deploy on GitHub or GitLab.
Forgetting credential.UseHttpPath true. It produces a 403 when cloning from CodeCommit that looks
like an IAM permissions problem and is not. It is the first place to look.
Leaving the CodeConnections connection in PENDING. The resource exists, the CLI lists it and
everything that depends on it fails with a confusing message. Authorising in the browser is manual.
Storing long-lived keys as GitHub secrets. Use OIDC with a sub condition naming your organisation
and your repository; a repo:* is a serious hole.
Denying the MergeBranchesBy* actions to "protect main". Nobody can merge, not even an approved PR,
and two days later somebody asks for the whole policy to be removed. Protect direct writing, not
governed merging.
Feature branches that live for three weeks. Every extra day multiplies the merge cost. If the change is big, split it into backwards-compatible increments. And adopting GitFlow out of habit in a continuous delivery product adds three long-lived branches in exchange for a problem you do not have.
Committing a .env "just for a moment to test". It stays in the history forever and the only correct
answer is to rotate. There is no shortcut.
Merging with squash and then trying to revert an intermediate commit. It no longer exists: the whole branch is a single commit. That is the conscious price of squashing.
Tip: write CONTRIBUTING.md on day one, and use .github/pull_request_template.md to preload the
four sections. Half a page saves the same five conversations every month, and a reviewer who has context
reviews twice as fast.
Tip: enable automatic branch deletion on merge, and tag every version that reaches production without exception: on the day of the incident, the difference between ten seconds and twenty minutes of diagnosis is in that tag.
Exercises
Exercise 1: choosing a provider with criteria
For each scenario, choose between CodeCommit, GitHub, self-hosted GitLab or Bitbucket and justify it in three sentences naming the decisive criterion:
(a) Spanish public-sector organisation, 40 developers. The tender requires the code to reside on infrastructure under their control inside the EU with full access auditing. They have had an AWS account since 2019 with CodeCommit enabled and 60 repositories.
(b) A 6-person startup starting today. They all come from GitHub, they want dependency scanning without having to set it up and they will deploy on AWS with Lambda and S3.
(c) A 25-person team in a company that uses Jira for everything, with quarterly audits that cross-reference issues with commits.
Exercise 2: the policy that blocked the team
Marta applies this to the desarrolladores-mercadofresco group to protect main:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "codecommit:GitPush",
"Resource": "arn:aws:codecommit:eu-west-1:111122223333:mercadofresco-tienda",
"Condition": {
"StringNotEqualsIfExists": { "codecommit:References": ["refs/heads/main"] }
}
}]
}The next day Luis cannot push to his feature branch, but he can push to main.
Answer: (a) what it does exactly and why it produces that effect; (b) write it correctly;
(c) what it is missing in order to protect main properly; (d) what check would have caught the
mistake before applying it.
Exercise 3: the migration with a surprise
When running step 2, Luis finds: a .env in 312 commits with the mfadmin password and the gateway key;
an 18 MB clientes_octubre.csv with 4,200 real names, addresses and phone numbers, added nine months ago;
and an experimento/pagos-bizum branch from a year ago that was never merged.
Answer: (a) what you do with each finding and in what order; (b) which one makes rewriting the history stop being optional, and why; (c) the exact command and what you warn the team about first; (d) who inside the company has to be informed about the second finding.
Solutions
Solution 1
(a) CodeCommit, and it is the only scenario in the exercise where it makes sense. The decisive
criterion is the first on the list: the legal requirement for residency and control of the code, which
overrides the others. The account predates July 2024, so the service is available; the code lives in their
AWS account in eu-west-1, encrypted with their KMS key, and CloudTrail gives the auditing the tender
asks for. The alternative would be self-hosted GitLab in their VPC, which meets the requirement just
as well but forces them to operate and patch the server: with 60 repositories already migrated it is not
worth it. What they should do is plan the exit, because they are on a service with no evolution.
(b) GitHub. Decisive criterion: where the team already is, reinforced by the ecosystem. Six people who already know the tool are productive on day one; Dependabot gives dependency scanning with nothing to set up; private repositories are free at that size. To deploy on AWS, OIDC plus CodeConnections covers everything without storing keys, and there is no residency restriction to complicate it.
(c) Bitbucket. Decisive criterion: integration with the rest of the tooling. The native linking between commits, branches and Jira issues is exactly what their quarterly audit consumes; with GitHub they would have to build and maintain that integration. GitLab would be better at CI/CD, but here the saving is in the process, not in the pipeline; and at 25 people Bitbucket is also the cheapest.
Solution 2
(a) The condition is inverted. StringNotEqualsIfExists with refs/heads/main means "deny when the
reference is not main". The result is literally the opposite of the one intended: it forbids pushing
to any branch except main and leaves main completely open. It is a one-word mistake — Not — with
consequences opposite to the intention, and that is why denials with negated conditions have to be read
twice, out loud.
(b) The correct one uses StringEqualsIfExists to narrow the denial to main, adds Null so that it
does not affect operations with no reference, and broadens the actions:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenegarEscrituraDirectaEnMain",
"Effect": "Deny",
"Action": ["codecommit:GitPush", "codecommit:DeleteBranch",
"codecommit:PutFile", "codecommit:DeleteFile"],
"Resource": "arn:aws:codecommit:eu-west-1:111122223333:mercadofresco-tienda",
"Condition": {
"StringEqualsIfExists": { "codecommit:References": ["refs/heads/main"] },
"Null": { "codecommit:References": "false" }
}
}]
}(c) DeleteBranch stops main being deleted entirely; PutFile and DeleteFile close editing from
the web console, which bypasses Git and is the most forgotten hole. A push --force is already covered
because IAM does not distinguish it from a GitPush. But what is missing is what no policy can give: an
approval rule associated with the repository requiring at least one approval on PRs into main, plus —
when it exists — the check that the build is green. Without that, the policy only forces changes through a
PR; it does not force the PR to be good.
(d) The IAM policy simulator from 04-01, or better still: apply it first to a test repository and run
the four cases — push to a feature branch, push to main, branch deletion, editing from the console —
checking that two pass and two fail. A denial with conditions must never make its debut in the real
repository: the failure mode is that it blocks the team or, worse, that it blocks nothing at all and
nobody finds out.
Solution 3
(a) The order matters because the first step makes the timing of the others irrelevant. One:
immediately rotate the .env secrets — the mfadmin password from Secrets Manager, the gateway key
regenerated in the provider's panel — before touching the repository; they have been compromised for
fourteen months and deleting them does not un-compromise them. Two: the CSV, decide on the rewrite and
upload nothing in the meantime. Three: the experimento/pagos-bizum branch is not migrated; if it has
value it is exported with git format-patch and kept outside. Migrating dead branches is dragging noise.
(b) The CSV. The .env is serious but bounded: it is resolved by rotating, because a rotated secret
is worth nothing even if it stays in the history. A file with 4,200 real names, addresses and phone
numbers cannot be rotated. It is personal data under the GDPR, and the minimisation principle plus the
right to erasure make it unacceptable to keep it indefinitely in a code repository, where it will be
replicated in every clone, every CI cache and every provider copy. Here rewriting stops being a technical
option and becomes an obligation.
(c)
cp -r ~/proyectos/tienda ~/proyectos/tienda-copia-seguridad # filter-repo is irreversible
cd ~/proyectos/tienda
git filter-repo --path datos/clientes_octubre.csv --invert-paths
git filter-repo --path .env --invert-paths # while we are at itgit filter-repo changes the hash of the affected commits and of all their descendants, so you have
to warn that any existing clone becomes useless and must be re-cloned: merging an old clone with the
rewritten history creates a disaster of duplicated commits. Here the migration has not happened yet and
only Luis's clone exists, so the impact is nil — an excellent reason to clean up before uploading. If
it were already on the remote, you would also need push --force, deletion of obsolete remote branches
and tags, and a request to the provider to purge the unreachable objects.
(d) The data protection officer (DPO or equivalent). Personal data of 4,200 customers outside the
authorised systems for nine months is unforeseen processing that has to be documented and assessed. It may
not be a breach notifiable to the AEPD — the repository was private and local — but that assessment is
not made by the technical team: you provide the complete information (what data, for how long, who had
access, what has been done) and whoever holds the responsibility decides. It is the same criterion we
applied in 06-03 with the aurora-mf-pruebas-luis clone.
Conclusion
MercadoFresco's code has left Luis's laptop. It is no longer a git init with a USB copy from September:
it is a remote repository, replicated, encrypted, with IAM permissions instead of system users and with at
least three live copies across the team. The first four risks in Marta's inventory are closed by the
simple fact of having uploaded the code.
But the important part of this lesson is not the hosting, it is the governance. You know that
CodeCommit no longer accepts new customers and why that does not invalidate learning it — you are going to
run into it —, and that the current answer is GitHub or GitLab connected with CodeConnections, with
its manual browser authorisation that breaks so many source actions when it is forgotten. You have the
choice criteria in order: first the legal requirement if there is one, then where your team already is,
then where you want the CI/CD to live, and last the ecosystem. You know how to authenticate with the
credential helper — with credential.UseHttpPath true, which avoids the mysterious 403 — and with
OIDC instead of long-lived keys for machines. And you know how to write a policy that protects main
without blocking the team, and why the Null condition always accompanies StringEqualsIfExists.
You have chosen trunk-based with a desarrollo branch declared as temporary scaffolding, because a
web shop has only one version — the one in production right now — and GitFlow solves a problem
MercadoFresco does not have. You know what a PR that is worth something looks like — what, why, how to
test it, risks —, when to use squash and when three-way, and how an approval rule template turns
review into a real control thanks to the fact that the author cannot approve themselves. You have the
conventions a pipeline will read later on: conventional commits, semantic versioning and tags
that answer "what is in production?" in ten seconds. And you have the rule that admits no exceptions:
the .env never gets committed, because it stays in the history forever and the answer is to rotate.
Now there is a repository with rules. What there is not is any guarantee that what goes into it
works. Approving the PR means that a person read the diff on a Tuesday afternoon, which is far more than
nothing and considerably less than enough. Nobody has run the tests: Luis runs them on his laptop "nearly
always", and the last time one failed he decided it was something to do with his environment. Nobody has
checked that the dependencies have no known vulnerabilities, nor that a key has not slipped into the code,
nor that the project even starts on a clean machine. The phrase "it works on my laptop" is still an
admissible argument at MercadoFresco, and there is still no artefact of any kind: what gets deployed is a
folder copied with scp.
In 08-02, "AWS CodeBuild", that comes to an end. We will see what continuous integration is and why it
turns "it works on my laptop" into a phrase with no value; the anatomy of a build project with its
environment, its service role, its cache and its artefacts; a complete, commented buildspec.yml with the
install, pre_build, build and post_build phases; how to inject secrets from Parameter Store and
Secrets Manager without them appearing in the logs; unit tests with pytest and coverage reports as a
quality gate rather than a report nobody reads; dependency scanning and leaked-credential scanning; the
building of the package that will go to the mercadofresco-artefactos bucket; and how to run the build
inside vpc-mercadofresco to test against the aurora-mf-pruebas-luis clone, with the cost warning that
comes with it.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
