One of the biggest pain points of using any CI/CD platform alongside a cloud service is managing credentials, and up until this week Github Actions was no exception. That all changes with the announcement of Github Actions OpenID Connect. With this feature Github Repositories can be given permissions directly from AWS IAM.
This is a really powerful tool that can save a lot of aggrevation and busy work when it comes to setting up CI/CD. I recently used it to simplify pushing images from Github to AWS ECR and thought others could benefit from the lessons I learned.
Setting up AWS
I'm going to use Terraform to manage the ECR Repositories. At the end we're going to have a workspace with a reusable ECR module, the OIDC setup, and a few repositories.
OIDC Resource
The first thing we need to do is connect Github's Open ID Connector to our AWS account using the Terraform aws_iam_openid_connect_provider resource. This is pretty straight forward but requires you dig around to find the right URL, Client List, and Thumbprint- these values are the same for every account, so you can copy and paste this over without problem.
And that's it! With that one simple resource you've linked your account to Github.
ECR Repository Module
By creating a separate module for building a ECR Repository we can avoid a lot of boilerplate when creating multiple repositories.
Variables
Before we start creating resources we need a few variables- specifically the Github Organization, the name of the ECR Repository (which should match the Github Repository), and the OIDC Provider we created above. If you are only using a single Organization then you can put it as the default to save some boilerplate.
Repository
Next we create the repository. As mentioned above we're going to give it the same name as the Git Repository.
Again pretty simple- just a single resource.
IAM Role
This is where the magic happens. Here we create the Role for the Github Action, doing a few very important things-
- Add the Github OpenID Connect Provider as the Principal for the role.
- Give Github the ability to assume this role by giving it the
sts:AssumeRoleWIthWebItentity
action. This is what actually allows Github to give this role to the Github Action. - Limit that access further with a condition against the repository name. After all, it wouldn't be a good idea to let any Github Action take on this role.
Another thing to note is the Role Name- we're going to need that later. We're using a standard naming scheme so we can easily refer to the role from inside of our Github Actions.
IAM Policy
Now we're at the point where we want to define the permissions that the Github Action has. In our example we're pushing to an ECR Repository, but this method can be used for any AWS resources- pushing to S3, updating an ECS Task Definition, and even running Terraform itself are all possible with this method.
The policy we use is locked down to the specific ECR Repository- the Github Action that uses this can only act on the single repository. The ecr:GetAuthorizationToken
permission is only needed to log in to the registry. Once the policy is created we attach it to the role from above.
Tying it Together
Now we've built out module, so lets head back to where we've defined our OIDC resource and create some repositories! To make this easy to maintain we put a list of repository names right at the top, and then use the Terraform for_each functionality to call the module for every name in that list.
If we were feeling really creative we could use the Github Terraform Provider to look up our repositories based on Organization and tags, but we'll leave that exercise for another day.
Github Actions
Assuming the AWS Role
Now that we're ready to run the actions on Github we need to assume the role we created for the repository. AWS has a published action for logging in that handles most of the work. Unfortunately this OIDC functionality is so new that they haven't released a working version yet, so we have to work directly from their master branch until they do. This functionality was released in v1.6.0, so make sure to tie to v1 or (if using a more specific tag) a version at or later than v1.6.0.
The most important value here is the IAM Role Name. Since we're using a standardized naming scheme across Github and ECR Repositories we can infer the role name instead of having to hard code it. That means the only things we need to provide are the AWS Account ID and the AWS Region that our container registry is in.
Pushing the Container
WIth all that out of the way here's full action to build and deploy an image to ECR. In this we're chaining together a variety of published actions from other vendors-
- actions/checkout to actually pull the repository.
- docker/setup-qemu-action to install an emulation layer to build multiplatform images.
- docker/setup-buildx-action to use the
docker buildx
system, again for multiplatform images. - aws-actions/configure-aws-credentials to log into AWS.
- aws-actions/amazon-ecr-login to log into AWS ECR.
- docker/metadata-action to create a bunch of tags for our docker container.
- docker/build-push-action to push the container.
Less Boilerplate, More Action
One thing you've probably noticed is that in all of that code there are only two unique values that can't be picked up with Github Context variables- the AWS Account and Region. This is a sign that this one off action could be turned into it's own standalone and reusable action. Using that action you end up with this much simpler action code.
Wrapping it Up
If you stuck with me this far you should have a good idea on how to link Github Actions to AWS, and maybe even some ideas on how to simplify your image building in general. If you have any questions please feel free to reach out- you can find me on twitter at @tedivm.
Thanks for the great write up of this functionality. One thing I have discovered from my implementation is that when using an ECR policy (resource based policy) which permits the IAM Federated user (in this case github actions), other IAM restrictions do not apply, as outlined in the policy evaluation logic described here:
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html
awesome post!!! I think there might be one typo in the terraform code
var.openid_connect_provider.arn
, probably started with that value as a variable, but then switched to the resource attributeThat’s actually because the Role is being created inside of the module that creates the ECR repo, so the resource is being passed in as a variable. This allows us to reuse the OIDC resource while still having a module for the ECR repo, which in turn lets us use the
for_each
resource when creating our repositories.I don’t understand how you get this ” AWS_ACCOUNT_ID: “999999999999” ” . can you give a insight on this.
That’s the AWS Account ID. Each account has a unique one, and you should put yours in there.
Thanks Robert, this article was really helpful!
Is there a folder structure that should be used when using these files, or a specific terraform version? When I put all the tf files in the same folder and I try to run terraform init (with the latest version, 1.1.4), I get
Initializing modules…
╷
│ Error: Invalid module source address
│
│ Module “repositories” (declared at main.tf line 14) has invalid source address “”: invalid source string: .
╵
Thanks for the great article!
Update: I think I put everything where it should go and got it at least terraform planning locally. Basically main.tf goes in the root ( which in my case is a “terraform” folder) and everything else goes under a modules/repositories folder (modules also being under the “terraform” folder) and the source referenced from the module is ‘./modules/repositories’ – I also did have to fix a typo or two (locals.repositories should be local.repositories in main.tf, and in github_iam_role.tf I had to change the identifiers line to identifiers = [var.oidc_arn] to match the passed-in module variable)
Hi Robert. Thanks for this excellent and detailed blog post. I managed to adapt it to our setup perfectly. Saved me tons of time. Thanks!