Foundation: IAM Service Roles and the S3 Artifact Bucket

K
Kai··5 min read·1 views

The previous article drew the picture: CodeBuild builds, CodeDeploy deploys, CodePipeline orchestrates. But those services don't automatically have permission to touch your resources — CodeBuild can't write logs or read code on its own, CodeDeploy can't push files to EC2 on its own. Each service needs to be granted permissions, and the way AWS does that is the service role. This article lays two foundation stones for the whole series: understanding and creating service roles, and standing up an S3 bucket to hold artifacts. All with the AWS CLI.

Goal

Understand how service roles and trust policies work (not just "create one to have one"), then create a service role for CodeBuild and an artifact bucket that meets exactly what the pipeline needs.

Service role: letting a service work on your behalf

When you call AWS, you use your own identity (IAM user, access key). But when CodeBuild runs a build, it acts on your behalf — it needs its own identity with exactly the permissions for the build, no more. That's a service role: an IAM role that an AWS service "borrows" for temporary permissions.

The mechanism lives in two parts of a role:

Trust policy answers "who is allowed to borrow this role?". For a CodeBuild service role, the trust policy says "the service codebuild.amazonaws.com is allowed to sts:AssumeRole." Without this line, no service can use the role.

Permissions policy answers "what can this role do?". These are the real permissions: write logs, read/write S3, pull code...

The core point, and the reason AWS works this way: when CodeBuild assumes the role, it receives temporary credentials (expiring after a few hours) instead of a fixed key stored somewhere. There's no access key to leak, no manual key rotation. Permissions are granted exactly when needed and revoke themselves.

   CodeBuild (AWS service)
        │  "I need to work" → sts:AssumeRole
        ▼
   ┌──────────────── awscicd-codebuild-role ─────────────────┐
   │  trust policy : who can assume? → codebuild.amazonaws.com
   │  permissions  : what can it do? → write logs, S3 artifact, GitPull
   └──────────────────────────┬──────────────────────────────┘
        │  grants TEMPORARY credentials (a few hours, auto-expire)
        ▼
   CodeBuild uses those permissions to run the build — no key stored

S3 bucket for artifacts

The pipeline needs a place for artifacts to pass through: CodeBuild finishes a build and produces a bundle (the built code), stored on S3; CodeDeploy pulls that bundle out to deploy. A bucket sits in the middle as the transit store.

$ BUCKET="awscicd-artifacts-$(aws sts get-caller-identity --query Account --output text)-ap-southeast-1"

$ aws s3api create-bucket --bucket "$BUCKET" --region ap-southeast-1 \
    --create-bucket-configuration LocationConstraint=ap-southeast-1
{
    "Location": "http://awscicd-artifacts-111122223333-ap-southeast-1.s3.amazonaws.com/",
    "BucketArn": "arn:aws:s3:::awscicd-artifacts-111122223333-ap-southeast-1"
}

Two configurations are mandatory for this bucket. First, enable versioning — this is not optional: CodePipeline requires the artifact bucket to have versioning, because it references artifacts by object version to know exactly which build is running:

$ aws s3api put-bucket-versioning --bucket "$BUCKET" \
    --versioning-configuration Status=Enabled

Second, block public access — artifacts are built source code, which should never be exposed to the Internet:

$ aws s3api put-public-access-block --bucket "$BUCKET" \
    --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Creating the CodeBuild service role

Start with the trust policy — a JSON file stating who may assume the role. Save it as cb-trust.json:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "codebuild.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}

Create the role with that trust policy:

$ aws iam create-role --role-name awscicd-codebuild-role \
    --assume-role-policy-document file://cb-trust.json \
    --description "Service role for CodeBuild (awscicd series)" \
    --query 'Role.[RoleName,Arn]' --output text
awscicd-codebuild-role  arn:aws:iam::111122223333:role/awscicd-codebuild-role

The role just created can't do anything yet — it only has a trust policy (who can borrow it), not permissions (what it can do). Attach a permissions policy. A minimal build needs: write logs to CloudWatch, read/write artifacts on the bucket just created, and pull code from CodeCommit (the series source). Save cb-perms.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Logs",
      "Effect": "Allow",
      "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:*:*:log-group:/aws/codebuild/*"
    },
    {
      "Sid": "ArtifactBucket",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:GetBucketLocation"],
      "Resource": [
        "arn:aws:s3:::awscicd-artifacts-111122223333-ap-southeast-1",
        "arn:aws:s3:::awscicd-artifacts-111122223333-ap-southeast-1/*"
      ]
    },
    {
      "Sid": "PullFromCodeCommit",
      "Effect": "Allow",
      "Action": ["codecommit:GitPull"],
      "Resource": "*"
    }
  ]
}
$ aws iam put-role-policy --role-name awscicd-codebuild-role \
    --policy-name awscicd-codebuild-permissions \
    --policy-document file://cb-perms.json

Note the least-privilege style: the log permission is scoped to CodeBuild's log group only (/aws/codebuild/*), the S3 permission only on the exact artifact bucket, not spread across the whole account. One role per service, each role with just enough permission for its job — when something goes wrong, the blast radius is narrow.

Verification

View the trust policy (who can assume) and the attached permissions:

$ aws iam get-role --role-name awscicd-codebuild-role \
    --query 'Role.AssumeRolePolicyDocument'
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": { "Service": "codebuild.amazonaws.com" },
            "Action": "sts:AssumeRole"
        }
    ]
}

$ aws iam list-role-policies --role-name awscicd-codebuild-role --output text
POLICYNAMES awscicd-codebuild-permissions

$ aws s3api get-bucket-versioning --bucket "$BUCKET" --output text
Enabled

Trust policy points at the right service, permissions are attached, the bucket has versioning enabled. The foundation is ready. CodeDeploy and CodePipeline also need their own service roles, but we'll create those in the very articles that introduce each service (Article 8 and Article 12), where the specific permissions have context to make sense — instead of dumping them all here.

🧹 Cleanup

The artifact bucket and the CodeBuild role are a shared foundation for the whole series, so keep them until you're done. When you want to delete them (at the end of the series), run:

$ aws iam delete-role-policy --role-name awscicd-codebuild-role --policy-name awscicd-codebuild-permissions
$ aws iam delete-role --role-name awscicd-codebuild-role
$ aws s3 rm "s3://$BUCKET" --recursive    # delete objects first
$ aws s3api delete-bucket --bucket "$BUCKET"

An IAM role and an empty S3 bucket cost almost nothing, so keeping them across the articles is fine; just remember to clean up at the end.

Wrap-up

A service role is how AWS grants a service permission to act on your behalf: the trust policy decides who can assume it (e.g. codebuild.amazonaws.com), the permissions policy decides what it can do, and when assumed the service receives temporary credentials instead of a fixed key. We created a CodeBuild role following least-privilege (logs + S3 artifact + GitPull) and an S3 artifact bucket with versioning enabled (required by CodePipeline) and public access blocked. This is the foundation everything later relies on.

The next article builds the code source: create a CodeCommit repo, set up Git credentials to push, push a sample app, then walk through branches and pull requests — all with the AWS CLI and Git.