How to invoke a new EC2 instance with existing IAM role? / JavaScript AWS SDK v3 - RunInstancesCommand - amazon-web-services

How can I launch an EC2 instance with attached (existing) IAM Role in JavaScript?
I know this question has been asked for other languages, but I couldn't find an example for JavaScript.
My JS Lambda code currently looks like:
const { EC2Client } = require( "#aws-sdk/client-ec2");
const ec2Client = new EC2Client({ region: "eu-central-1" });
const {
CreateTagsCommand,
RunInstancesCommand,
TerminateInstancesCommand,
RunInstancesRequest
} = require("#aws-sdk/client-ec2");
const instanceParams = {
ImageId: "ami-00a844bXXXXXXXXXX",
InstanceType: "a1.xlarge",
KeyName: "YourKeyName",
MinCount: 1,
MaxCount: 1,
SecurityGroupIds: ["sg-XXXXXXXXXXXXX"],
IamInstanceProfile: "arn:aws:iam::390000000000:instance-profile/NAME" // doesn't work
};
const run = async () => {
try {
const data = await ec2Client.send(new RunInstancesCommand(instanceParams));
console.log(data.Instances[0].InstanceId);
} catch (err) {
console.log("Error", err);
}
}
var ret = await run();
Documentation:
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-ec2/interfaces/runinstancescommandinput.html#iaminstanceprofile
"#aws-sdk/client-ec2": "^3.44.0",
"#aws-sdk/client-lambda": "^3.45.0",
The code above runs, but the IAM Role under the EC2 > Instances > Security tab remains empty in all my tries.

For IamInstanceProfile field you need to specify the ARN or name. Try with any of these options:
const instanceParams = {
ImageId: "ami-00a844bXXXXXXXXXX",
InstanceType: "a1.xlarge",
KeyName: "YourKeyName",
MinCount: 1,
MaxCount: 1,
SecurityGroupIds: ["sg-XXXXXXXXXXXXX"],
IamInstanceProfile:
{
Name: "NAME"
}
};
or
const instanceParams = {
ImageId: "ami-00a844bXXXXXXXXXX",
InstanceType: "a1.xlarge",
KeyName: "YourKeyName",
MinCount: 1,
MaxCount: 1,
SecurityGroupIds: ["sg-XXXXXXXXXXXXX"],
IamInstanceProfile:
{
Arn: "arn:aws:iam::390000000000:instance-profile/NAME"
}
};
JavaScript SDK runInstances

Related

Elastic Beanstalk manual deploy application version S3 error

When I try to deploy another application version:
I get the following S3 error message:
For test purposes, the attached aws-service-role, instance profile and the user doing the manual deploy have full S3 access.
The service and instance roles look like the following:
createServiceRole(): Role {
const assumedBy = new ServicePrincipal("elasticbeanstalk.amazonaws.com")
return new Role(this, `${this.settings.prefix}-service-role-${this.env}-id`, {
roleName: `${this.settings.serviceName}-service-role-${this.env}`,
assumedBy: assumedBy,
managedPolicies: [
{managedPolicyArn: "arn:aws:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk"},
{managedPolicyArn: "arn:aws:iam::aws:policy/AmazonS3FullAccess"}
]
})
}
createInstanceProfile(): CfnInstanceProfile {
const assumedBy = new ServicePrincipal("ec2.amazonaws.com")
const role = new Role(this, `${this.settings.serviceName}-access-role-${this.env}-id`, {
roleName: `${this.settings.serviceName}-instance-profile-role-${this.env}`,
assumedBy: assumedBy,
managedPolicies: [
{managedPolicyArn: "arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier"},
{managedPolicyArn: "arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier"},
{managedPolicyArn: "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"},
{managedPolicyArn: "arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker"},
{managedPolicyArn: "arn:aws:iam::aws:policy/AmazonS3FullAccess"}
]
})
return new CfnInstanceProfile(this, `${this.settings.serviceName}-instance-profile-${this.env}-id`, {
instanceProfileName: `${this.settings.serviceName}-instance-profile-${this.env}`,
roles: [role.roleName]
})
}
Any idea?

Cannot sts:AssumeRole with a service account for CDK-generated EKS cluster

Having deployed an EKS 1.21 cluster using CDK, then using https://cert-manager.io/docs/installation/ as a guide, I have attempted to install cert-manager with the end goal of using Let's Encrypt certificates for TLS-enabled services.
Creating IAM policies in my Stack's code:
...
var externalDnsPolicy = new PolicyDocument(
new PolicyDocumentProps
{
Statements = new[]
{
new PolicyStatement(
new PolicyStatementProps
{
Actions = new[] { "route53:ChangeResourceRecordSets", },
Resources = new[] { "arn:aws:route53:::hostedzone/*", },
Effect = Effect.ALLOW,
}
),
new PolicyStatement(
new PolicyStatementProps
{
Actions = new[]
{
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
},
Resources = new[] { "*", },
Effect = Effect.ALLOW,
}
),
}
}
);
var AllowExternalDNSUpdatesRole = new Role(
this,
"AllowExternalDNSUpdatesRole",
new RoleProps
{
Description = "Route53 External DNS Role",
InlinePolicies = new Dictionary<string, PolicyDocument>
{
["AllowExternalDNSUpdates"] = externalDnsPolicy
},
RoleName = "AllowExternalDNSUpdatesRole",
AssumedBy = new ServicePrincipal("eks.amazonaws.com"),
}
);
var certManagerPolicy = new PolicyDocument(new PolicyDocumentProps {
Statements = new []
{
new PolicyStatement(new PolicyStatementProps
{
Effect = Effect.ALLOW,
Actions = new []
{
"route53:GetChange",
},
Resources = new []
{
"arn:aws:route53:::change/*",
}
}),
new PolicyStatement(new PolicyStatementProps
{
Effect = Effect.ALLOW,
Actions = new []
{
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
},
Resources = new []
{
"arn:aws:route53:::hostedzone/*",
},
}),
},
});
var AllowCertManagerRole = new Role(
this,
"AllowCertManagerRole",
new RoleProps
{
Description = "Route53 Cert Manager Role",
InlinePolicies = new Dictionary<string, PolicyDocument>
{
["AllowCertManager"] = certManagerPolicy
},
RoleName = "AllowCertManagerRole",
AssumedBy = new ServicePrincipal("eks.amazonaws.com"),
}
);
...
And my cert issuer manifest:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: cert-issuer
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::XREMOVEDX:role/AllowCertManagerRole
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cert-issuer-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cert-issuer
subjects:
- kind: ServiceAccount
name: cert-issuer
namespace: default
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: sometls-net-letsencrypt
spec:
acme:
email: domain#sometls.net
preferredChain: ""
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: sometls-net-letsencrypt-account-key
solvers:
- dns01:
route53:
hostedZoneID: Z999999999999
region: us-east-2
role: arn:aws:iam::XREMOVEDX:role/AllowExternalDNSUpdatesRole
selector:
dnsZones:
- sometls.net
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: sometls-cluster-lets-encrypt
spec:
secretName: somtls-cluster-lets-encrypt
issuerRef:
name: sometls-net-letsencrypt
kind: ClusterIssuer
group: cert-manager.io
subject:
organizations:
- sometls
dnsNames:
- "*.sometls.net"
But I'm getting spammed with these errors, and cert-manager doesn't work:
(combined from similar events): Error presenting challenge: error instantiating route53 challenge solver: unable to assume role: AccessDenied: User: arn:aws:sts::XREMOVEDX:assumed-role/EksStackEast-EksClusterNodegroupDefaultC-U7IJ1PNZ2123/i-007c425b7a5e39123 is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::XREMOVEDX:role/AllowCertManagerRole status code: 403, request id: 2bd885a2-97a0-4a21-b017-40e099cb4123
I'm very very iffy on how the IAM Roles allow the Kubernetes ServiceAccount to assume them. I must be missing some connection piece that lets the magic of EKS IAM Role for Service Accounts (IRSA) happen.
Please help!
UPDATE: Using CfnJson I am able to create the role so it looks like this:
{
"Role": {
"Path": "/",
"RoleName": "AllowCertManagerRole",
"RoleId": "REDACTED",
"Arn": "arn:aws:iam::REDACTED:role/AllowCertManagerRole",
"CreateDate": "2022-03-24T21:42:32+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::REDACTED:oidc-provider/oidc.eks.us-east-2.amazonaws.com/id/REDACTED"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"oidc.eks.us-east-2.amazonaws.com/id/REDACTED:sub": "system:serviceaccount:*:cert-issuer"
}
}
}
]
},
"Description": "Route53 Cert Manager Role",
"MaxSessionDuration": 3600,
"Tags": [
{
"Key": "dynasty",
"Value": "sometls-1.0"
}
],
"RoleLastUsed": {}
}
}
I'm still getting the same errors. The condition in the new Role uses the "StringLike" operator. Not sure if that is correct or not, and I'm not sure how to avoid needing to use a non-derived lvalue when setting up the IDictionary<string, object> for the conditions. Also-- the error message is the same in that it expects to be able to sts:AssumeRole not sts:AssumeRoleWithWebIdentity ... I tried changing the action in the Role to sts:AssumeRole with the same effect.
UPDATE #2:
The actual problem with cert-manager was a modification to the install manifests that I missed required for AWS IRSA to work. https://cert-manager.io/docs/configuration/acme/dns01/route53/#service-annotation ... turns out that is really important.
For anyone who wants to see how to add an OIDC provider as a AssumedBy principal with Conditions in C# see snip below. I would have thought there would be a convenience method in AWS CDK that would take care of these machinations automatically. I couldn't find it...
...
var Cluster = new Cluster(this,"EksCluster", new ClusterProps
{ ... });
...
var CertIssuerCondition = new CfnJson(this, "CertIssuerCondition", new CfnJsonProps
{
Value = new Dictionary<string, object>
{
{$"{Cluster.ClusterOpenIdConnectIssuer}:sub", "system:serviceaccount:*:cert-manager"},
}
});
var certManagerPolicy = new PolicyDocument(new PolicyDocumentProps {
Statements = new []
{
new PolicyStatement(new PolicyStatementProps
{
Effect = Effect.ALLOW,
Actions = new []
{
"route53:GetChange",
},
Resources = new []
{
"arn:aws:route53:::change/*",
}
}),
new PolicyStatement(new PolicyStatementProps
{
Effect = Effect.ALLOW,
Actions = new []
{
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
},
Resources = new []
{
"arn:aws:route53:::hostedzone/*",
},
}),
new PolicyStatement(new PolicyStatementProps
{
Effect = Effect.ALLOW,
Actions = new[]
{
"route53:ListHostedZonesByName",
},
Resources = new[]
{
"*",
}
}),
},
});
var AllowCertManagerRole = new Role(
this,
"AllowCertManagerRole",
new RoleProps
{
Description = "Route53 Cert Manager Role",
InlinePolicies = new Dictionary<string, PolicyDocument>
{
["AllowCertManager"] = certManagerPolicy
},
RoleName = "AllowCertManagerRole",
AssumedBy = new FederatedPrincipal(Cluster.OpenIdConnectProvider.OpenIdConnectProviderArn, new Dictionary<string, object>
{
["StringLike"] = CertIssuerCondition,
},"sts:AssumeRoleWithWebIdentity")
}
);
The trust relationship of your IAM role looks wrong to me.
You need to use a federated principal pointing to the OIDC provider of your EKS cluster, ideally with a condition that correctly reflects your service account and namespace names.
The principal has to look something like this:
const namespaceName = 'cert-manager'
const serviceAccountName = 'cert-issuer'
// If you're deploying EKS with CloudFormation/CDK you could for example export the OIDC provider ARN and get it with Fn.importValue(...) in your stack.
const oidcProviderUrl = 'oidc.eks.YOUR-REGION.amazonaws.com/id/REDACTED';
// You can use wildcards for the namespace name and/or service account name if you want to have a less restrictive condition.
const conditionValue = `system:serviceaccount:${namespaceName}:${serviceAccountName}`;
const roleCondition = new CfnJson(this.stack, `CertIssuerRoleCondition`, {
value: { [`${oidcProviderUrl}:sub`]: conditionValue }
});
// If you're deploying EKS with CloudFormation/CDK you could for example export the OIDC provider ARN and get it with Fn.importValue(...) in your stack.
const oidcProviderArn = 'arn:aws:iam::REDACTED:oidc-provider/oidc.eks.YOUR-REGION.amazonaws.com/id/REDACTED';
const principal = new FederatedPrincipal(oidcProviderArn, roleCondition, 'sts:AssumeRoleWithWebIdentity');
// Now use that principal for your IAM role.

How to avoid parameterized cloud formation from CDK synth

I am working on a CDK app in which I have to create a VPC and EKS cluster. but I am not directly using the CDK to run the cloudformation. I want to create a cloudformation template separately and run it using AWS CLI. But whenever I am creating the cloudformation template EKS has asset parameters, which cause error for me while I run the template. How to avoid those parameters.
These are my files.
bin/eks.ts
#!/usr/bin/env node
import cdk = require('#aws-cdk/core');
import { VPCStack, EKSStack } from '../lib/eks-stack';
import { Construct, TagManager, Tag } from '#aws-cdk/core';
import { Scope } from 'babel__traverse';
const app = new cdk.App();
const environment_variables = { region: "us-east-1", account: "348394859384543" }
const appVPCStack = new VPCStack(app, "appDemoVPCStack", { env: environment_variables })
Tag.add(appVPCStack, "owner", "tamizh");
Tag.add(appVPCStack, "purpose", "testing");
const appEKSStack = new EKSStack(app, "appDemoEKSStack", { env: environment_variables, vpcStack: appVPCStack })
Tag.add(appEKSStack, "owner", "tamizh");
Tag.add(appEKSStack, "purpose", "testing");
app.synth();
lib/eks.ts
import cdk = require('#aws-cdk/core');
import ec2 = require('#aws-cdk/aws-ec2');
import { DefaultInstanceTenancy, GatewayVpcEndpointAwsService, GatewayVpcEndpoint } from '#aws-cdk/aws-ec2';
import { ManagedPolicy } from '#aws-cdk/aws-iam';
import eks = require('#aws-cdk/aws-eks');
import iam = require('#aws-cdk/aws-iam');
import asg = require("#aws-cdk/aws-autoscaling");
import { TagManager, TagType } from '#aws-cdk/core';
export class VPCStack extends cdk.Stack {
public readonly vpc: ec2.Vpc;
private endpoint: GatewayVpcEndpoint;
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const eksClusterName = this.node.tryGetContext("eks.clusterName");
this.vpc = new ec2.Vpc(this, eksClusterName+'VPC', {
// VPC configurations
})
}
}
export interface EKSProps extends cdk.StackProps {
vpcStack: VPCStack;
}
export class EKSStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: EKSProps) {
super(scope, id, props);
const vpc = props.vpcStack.vpc;
// Context variables for dynamic configuration
const current_env = this.node.tryGetContext("env.type");
const eksClusterName = this.node.tryGetContext("eks.clusterName");
const nodeGroupKeyName = this.node.tryGetContext("eks.nodeGroupKeyName");
const nodeGroupMaxCapacity = this.node.tryGetContext("eks.nodeGroupMaxCapacity");
const nodeGroupMinCapacity = this.node.tryGetContext("eks.nodeGroupMinCapacity");
const nodeGroupDesiredCapacity = this.node.tryGetContext("eks.nodeGroupDesiredCapacity");
const nodeGroupInstanceType = this.node.tryGetContext("eks.nodeGroupInstanceType");
// Role to access the cluster from using kubeconfig
// aws eks update-kubeconfig --name eks --region <region> --role-arn <role-arn>
const clusterAdmin = new iam.Role(this, eksClusterName+'AdminRole', {
assumedBy: new iam.AccountRootPrincipal()
});
// Cluster properties
const clusterProps = {
clusterName: current_env+eksClusterName,
// Default capacity as 0 denotes infinite number of worker nodes
// To avoid allocate the max number worker node while creating control plane
defaultCapacity: 0,
vpc: vpc,
mastersRole: clusterAdmin
}
// Create a new EKS cluster control plane
const cluster = new eks.Cluster(this, eksClusterName, clusterProps);
const eksOptimizedImage = {
//standard or GPU-optimized
nodeType: eks.NodeType.STANDARD
};
const nodeGroupMachineImage = new eks.EksOptimizedImage(eksOptimizedImage);
// defining autoscaling group for worker nodes which can be scalled up or down at any time
const rcAsg = new asg.AutoScalingGroup(this, current_env+'ASG', {
vpc: vpc,
instanceType: nodeGroupInstanceType,
machineImage: nodeGroupMachineImage,
// Create a keypair to ssh into the worker nodes and give the keypair here
// It should be same account and region
// keyName: nodeGroupKeyName,
minCapacity: nodeGroupMinCapacity,
maxCapacity: nodeGroupMaxCapacity,
desiredCapacity: nodeGroupDesiredCapacity,
updateType: asg.UpdateType.ROLLING_UPDATE,
vpcSubnets: {subnetType: ec2.SubnetType.PRIVATE}
});
cluster.addAutoScalingGroup(rcAsg, {
mapRole: true
})
}
}
Once I ran this, got output template as bellow.
{
"Transform": "AWS::Serverless-2016-10-31",
"Resources": {
// all resources
},
"Parameters": {
"AssetParametersea4957b1606259534983fnjdfs934r4b6ad19a204S3Bucket371D99F8": {
"Type": "String",
"Description": "S3 bucket for asset \"ea4957b1606m93439fmrefew99cc02944b6ad19a204\""
},
// more parameters.
}
}
How to avoid these asset parameters?
The answer is you can't avoid the asset parameters in CDK apps. Not every app has assets, whenever there is a functionality that cannot be done by plain cloudformation then they introduce these asset parameters which will be used during the CDK deploy time.
reference - https://github.com/aws/aws-cdk/issues/5403

AWS CloudFormation outputs to JenkinsFile pipeline

I have cloudFormation.yaml and JenkinsFile.groovy.
In cloudFormation has outputs:
Outputs:
bucketName:
Value: !Ref BucketName
Description: 'bucket name'
I run cloudFormation:
stage("run cloudFormation") {
steps {
script {
withAWS(credentials: awsCredentials) {
cfnUpdate(
stack: stackName,
file: "cloudFormation.yaml",
params: [
"uniqString=${uniqString}"
],
timeoutInMinutes: 10,
pollInterval: 600
)
}
}
}
}
How can I get outputs backetName in my JenkinsFile?
I believe you can just assign it to a var
def outputs = cfnUpdate(...)
see the link below for further information:
https://github.com/jenkinsci/pipeline-aws-plugin/blob/master/README.md

How can I access an EC2 instance created by CDK?

I am provisioning an infrastructure using this CDK class:
// imports
export interface AppStackProps extends cdk.StackProps {
repository: ecr.Repository
}
export class AppStack extends cdk.Stack {
public readonly service: ecs.BaseService
constructor(app: cdk.App, id: string, props: AppStackProps) {
super(app, id, props)
const vpc = new ec2.Vpc(this, 'main', { maxAzs: 2 })
const cluster = new ecs.Cluster(this, 'x-workers', { vpc })
cluster.addCapacity('x-workers-asg', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO)
})
const logging = new ecs.AwsLogDriver({ streamPrefix: "x-logs", logRetention: logs.RetentionDays.ONE_DAY })
const taskDef = new ecs.Ec2TaskDefinition(this, "x-task")
taskDef.addContainer("x-container", {
image: ecs.ContainerImage.fromEcrRepository(props.repository),
memoryLimitMiB: 512,
logging,
})
this.service = new ecs.Ec2Service(this, "x-service", {
cluster,
taskDefinition: taskDef,
})
}
}
When I check the AWS panel, I see the instance created without a key pair.
How can I access the instance in this case?
You can go to your AWS account > EC2 > Key Pairs, create a new key pair and then you can pass the key name to the cluster being created:
const cluster = new ecs.Cluster(this, 'x-workers', { vpc })
cluster.addCapacity('x-workers-asg', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
keyName: "x" // this can be a parameter if you prefer
})
I got this idea by reading this article on CloudFormation.